diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt index 61a935da81..c1e199892b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt @@ -51,42 +51,6 @@ class SyncFlowTest : BaseTest() { ApplicationProvider.getApplicationContext().mutableWorkInfoStatuses } - @Test - fun shouldSuccessfullySyncAChangedTEI() { - val teiName = "Scott" - val teiLastName = "Kelley" - - prepareTBProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { - clickOnOpenSearch() - typeAttributeAtPosition(teiName, 0) - typeAttributeAtPosition(teiLastName, 1) - clickOnSearch() - clickOnTEI(teiName, teiLastName) - } - - teiDashboardRobot { - clickOnGroupEventByName(TB_VISIT) - clickOnEventWith(TB_VISIT_EVENT_DATE, ORG_UNIT) - } - - eventRobot { - clickOnUpdate() - } - - teiDashboardRobot { - composeTestRule.onNodeWithText("Sync").performClick() - } - syncFlowRobot { - waitToDebounce(500) - clickOnSyncButton(composeTestRule) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - checkSyncWasSuccessfully(composeTestRule) - } - cleanLocalDatabase() - } - @Test fun shouldShowErrorWhenTEISyncFails() { val teiName = "Lars" diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt index 6713922c09..343db8ad21 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt @@ -31,6 +31,7 @@ import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel fun searchTeiRobot(searchTeiRobot: SearchTeiRobot.() -> Unit) { @@ -186,7 +187,7 @@ class SearchTeiRobot : BaseRobot() { composeTestRule.setContent { ListCard( - title = title, + title = ListCardTitleModel(text = title), additionalInfoList = displayedAttributes, onCardClick = { } ) diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt new file mode 100644 index 0000000000..29af01483d --- /dev/null +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt @@ -0,0 +1,184 @@ +package org.dhis2.usescases.teidashboard + +import android.view.View +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.activityScenarioRule +import dhis2.org.analytics.charts.Charts +import io.reactivex.Observable +import org.dhis2.R +import org.dhis2.android.rtsm.utils.NetworkUtils +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.ui.ThemeManager +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.EventInitialTest +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository +import org.dhis2.usescases.main.program.ProgramPresenter +import org.dhis2.usescases.teiDashboard.DashboardRepositoryImpl +import org.dhis2.usescases.teiDashboard.DashboardViewModel +import org.dhis2.usescases.teiDashboard.TeiAttributesProvider +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts +import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity +import org.dhis2.utils.analytics.AnalyticsHelper +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventEditableStatus +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Calendar + +class TeiDashboardMobileActivityTest { + + @get:Rule + val activityScenarioRule = activityScenarioRule() + + @get:Rule + val composeRule = createAndroidComposeRule() + + + private lateinit var viewModel: DashboardViewModel + + private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) + private val resources: ResourceManager = mock() + private val charts: Charts = mock() + private val teiAttributesProvider: TeiAttributesProvider = mock() + + + private var repository: DashboardRepositoryImpl = mock { + + } + var tei = Observable.just(TrackedEntityInstance.builder() + .uid(TEI_Uid) + .created(Calendar.getInstance().time) + .lastUpdated(Calendar.getInstance().time) + .organisationUnit(ORG_UNIT_UID) + .trackedEntityType(TETYPE_NAME) + .build()) + + private val teType: TrackedEntityType = mock() + + private val analyticsHelper = mock { + } + + private val themeManager: ThemeManager = mock() + private val presenter: TeiDashboardContracts.Presenter = mock() + private val filterManager: FilterManager = mock() + private val networkUtils: NetworkUtils = mock() + + companion object { + const val ENROLLMENT_UID = "enrollmentUid" + const val TEI_Uid = "TEIUid" + const val PROGRAM_UID = "programUid" + const val TETYPE_NAME = "TETypeName" + const val INITIAL_ORG_UNIT_UID = "initialOrgUnitUid" + const val PROGRAM_STAGE_NAME = "Marvellous Program Stage" + const val EXECUTION_DATE = "Date of Marvellous Program Stage" + const val ORG_UNIT_UID = "orgUnitUid" + const val ENROLLMENT_VALUE_WITH_NOTE = "EnrollmentValueWithNote" + const val TEI_UID_VALUE_WITH_NOTE = "TeiUidValueWithNote" + const val CHILD_PROGRAM_UID_VALUE = "childProgramUid" + } + + private fun initViewModel() { + viewModel = DashboardViewModel( + repository , + analyticsHelper + ) + + } + + private fun setUp() { + initRepository() + initViewModel() + } + + private fun initRepository() { + repository = DashboardRepositoryImpl( + d2, + charts, + TEI_Uid, + PROGRAM_UID, + ENROLLMENT_UID, + resources, + teiAttributesProvider, + ) + + + } + + + @Test + fun shouldSuccessfullyInitializeTeiDashBoardMobileActivity() { + setUp() + whenever (repository.getTETypeName()) doReturn TETYPE_NAME + whenever ( repository.getTrackedEntityInstance("") ) doReturn mock() + whenever {repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + } } doReturn mock() + whenever {repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + }.blockingFirst() } doReturn { teType } + whenever( + presenter.teType + ) doReturn TETYPE_NAME + + whenever( + repository.getTETypeName() + ) doReturn TETYPE_NAME + whenever( + d2.trackedEntityModule() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("") ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() + .blockingGet() ) doReturn mock() + + whenever( + d2.trackedEntityModule().trackedEntityTypes() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("") ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() ) doReturn mock() + + + + + ActivityScenario.launch(TeiDashboardMobileActivity::class.java).onActivity { activity -> + + val showMoreOptions = activity.findViewById(R.id.moreOptions) + showMoreOptions.performClick() + } + + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt index 7e6ab907dd..d080db8ae1 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -168,17 +168,6 @@ class TeiDashboardTest : BaseTest() { } } - @Test - fun shouldNotBeAbleToCreateNewEventsWhenFull() { - prepareTeiOpenedWithFullEventsAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - checkCanNotAddEvent() - } - } - @Test fun shouldOpenEventAndSaveSuccessfully() { setupCredentials() @@ -235,32 +224,6 @@ class TeiDashboardTest : BaseTest() { } } - @Test - fun shouldSuccessfullyCreateANewEvent() { - prepareTeiToCreateANewEventAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - clickOnFab() - clickOnCreateNewEvent() - clickOnFirstReferralEvent() - waitToDebounce(2000) - clickOnReferralNextButton() - waitToDebounce(600) - } - - eventRobot { - fillRadioButtonForm(4) - clickOnFormFabButton() - clickOnNotNow(composeTestRule) - } - - teiDashboardRobot { - checkEventWasCreatedAndOpen(LAB_MONITORING, 0) - } - } - @Test fun shouldOpenEventEditAndSaveSuccessfully() { prepareTeiOpenedToEditAndLaunchActivity(rule) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a64207c11..aa604adfc6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,17 +70,6 @@ - - - - - - - - - , - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - cachedFields: List, - ): Flowable> { - return if (cachedFields.isNotEmpty()) { - updateEventFields(event, cachedFields, isEventEditable) - } else { - provideEventFields(event, programStageSections, isEventEditable, actionProcessor) - } - } - - private fun provideEventFields( - event: Event, - programStageSections: List, - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - ): Flowable> { - return Flowable.just(sortedStageDataElements(event.programStage()!!)) - .flatMapIterable { list -> list } - .map { programStageDataElement -> - stageDataElementToFieldViewModel( - programStageDataElement, - event.uid(), - programStageSections, - isEventEditable, - actionProcessor, - ) - }.toList().toFlowable() - } - - private fun updateEventFields( - event: Event, - fields: List, - isEventEditable: Boolean, - ): Flowable> { - return Flowable.just(fields) - .flatMapIterable { list -> list } - .map { fieldViewModel -> - - val de = dataElement(fieldViewModel.uid) - - val (rawValue, friendlyValue) = dataValue( - event.uid(), - fieldViewModel.uid, - fieldViewModel.valueType == ValueType.ORGANISATION_UNIT, - ) - - val error = checkConflicts( - event.uid(), - fieldViewModel.uid, - rawValue, - ) - - val legend = if (fieldViewModel.legend != null) { - getColorByLegend(rawValue, de) - } else { - null - } - - val updatedFieldViewModel = fieldViewModel.setValue(friendlyValue) - .setEditable(fieldViewModel.editable || isEventEditable) - .setLegend(legend) - .apply { - if (error.isNotEmpty()) { - setError(error) - } else { - setError(null) - } - } - - updatedFieldViewModel - }.toList().toFlowable() - } - - private fun sortedStageDataElements(stageUid: String): List { - val stageDataElements = stageDataElements(stageUid) - val stageSections = stageSections(stageUid) - if (stageSections.isNotEmpty()) { - val dataElementsOrder = arrayListOf() - stageSections.forEach { section -> - dataElementsOrder.addAll(getUidsList(section.dataElements()!!)) - } - stageDataElements.toMutableList().sortWith( - Comparator { de1: ProgramStageDataElement, de2: ProgramStageDataElement -> - val pos1 = dataElementsOrder.indexOf(de1.dataElement()!!.uid()) - val pos2 = dataElementsOrder.indexOf(de2.dataElement()!!.uid()) - pos1.compareTo(pos2) - }, - ) - } - return stageDataElements - } - - private fun stageDataElementToFieldViewModel( - programStageDataElement: ProgramStageDataElement, - eventUid: String, - programStageSections: List, - isEventEditable: Boolean, - actionProcessor: FlowableProcessor, - ): FieldUiModel { - val de = dataElement(programStageDataElement.dataElement()!!.uid()) - - val programStageSection: ProgramStageSection? = - programStageSections.firstOrNull { section -> - getUidsList(section.dataElements()!!).contains(de?.uid()) - } - - val optionSet = de?.optionSetUid() - - val (rawValue, friendlyValue) = dataValue( - eventUid, - de?.uid() ?: "", - de?.valueType() == ValueType.ORGANISATION_UNIT, - ) - - val optionSetConfiguration = options(optionSet) - - val error: String = checkConflicts(eventUid, de?.uid() ?: "", rawValue) - - val fieldViewModel: FieldUiModel = - fieldFactory.create( - de?.uid() ?: "", - de?.formName() ?: de?.displayName() ?: "", - de?.valueType()!!, - programStageDataElement.compulsory() == true, - de.optionSetUid(), - friendlyValue, - programStageSection?.uid(), - programStageDataElement.allowFutureDate() == true, - isEventEditable, - programStageSection?.renderType()?.mobile()?.type(), - de.displayDescription(), - programStageDataElement.renderType()?.mobile(), - de.style() ?: ObjectStyle.builder().build(), - de.fieldMask(), - optionSetConfiguration, - FeatureType.POINT, - ) - - return if (error.isNotEmpty()) { - fieldViewModel.setError(error) - } else { - fieldViewModel - } - } - - private fun stageDataElements(stageUid: String) = d2.programModule().programStageDataElements() - .byProgramStage().eq(stageUid) - .withRenderType().blockingGet() - - private fun stageSections(stageUid: String) = d2.programModule().programStageSections() - .byProgramStageUid().eq(stageUid) - .withDataElements() - .blockingGet() - - private fun dataElement(dataElementUid: String) = d2.dataElementModule().dataElements() - .withLegendSets() - .uid(dataElementUid) - .blockingGet() - - private fun dataValue( - eventUid: String, - dataElementUid: String, - isValueTypeOrgUnit: Boolean, - ): Pair { - val valueRepository = d2.trackedEntityModule().trackedEntityDataValues() - .value(eventUid, dataElementUid) - return if (valueRepository.blockingExists()) { - val value = valueRepository.blockingGet()?.value() - var friendlyValue = - valueRepository.blockingGetValueCheck(d2, dataElementUid).userFriendlyValue(d2) - if (value != null && isValueTypeOrgUnit) { - friendlyValue = "%s_ou_%s".format(value, friendlyValue) - } - Pair(value, friendlyValue) - } else { - Pair(null, null) - } - } - - private fun checkConflicts(eventUid: String, dataElementUid: String, value: String?): String { - val conflicts = d2.importModule().trackerImportConflicts() - .byEventUid().eq(eventUid) - .blockingGet() - - return conflicts.firstOrNull { conflict -> - conflict.event() == eventUid && - conflict.dataElement() == dataElementUid && - conflict.value() == value - }?.displayDescription() ?: "" - } - - private fun getColorByLegend(value: String?, dataElement: DataElement?): LegendValue? { - return if (value == null || dataElement == null) { - null - } else { - try { - if (dataElement.valueType()!!.isNumeric && - dataElement.legendSets() != null && - dataElement.legendSets()!!.isNotEmpty() - ) { - val legendSet = dataElement.legendSets()!![0] - var legend = - d2.legendSetModule().legends() - .byStartValue().smallerThan(java.lang.Double.valueOf(value)) - .byEndValue().biggerThan(java.lang.Double.valueOf(value)) - .byLegendSet().eq(legendSet.uid()) - .one() - .blockingGet() - if (legend == null) { - legend = d2.legendSetModule().legends() - .byEndValue().eq(java.lang.Double.valueOf(value)) - .byLegendSet().eq(legendSet.uid()) - .one() - .blockingGet() - } - if (legend != null) { - return LegendValue( - resourceManager.getColorFrom(legend.color()), - legend.displayName(), - ) - } - } - null - } catch (e: Exception) { - null - } - } - } - - private fun options(optionSetUid: String?): OptionSetConfiguration? = optionSetUid?.let { - OptionSetConfiguration.config( - d2.optionModule().options().byOptionSetUid().eq(it).blockingCount(), - ) { - d2.optionModule().options().byOptionSetUid().eq(it) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet() - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index dbbc2040ee..4dd1678f0e 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import org.dhis2.R import org.dhis2.commons.resources.ResourceManager @@ -51,8 +53,14 @@ fun ProvideInputDate( ) { if (uiModel.showField) { Spacer(modifier = Modifier.height(16.dp)) + val textSelection = TextRange(if (uiModel.eventDate.dateValue != null) uiModel.eventDate.dateValue.length else 0) + var value by remember(uiModel.eventDate.dateValue) { - mutableStateOf(uiModel.eventDate.dateValue?.let { formatStoredDateToUI(it) }) + if (uiModel.eventDate.dateValue != null) { + mutableStateOf(TextFieldValue(formatStoredDateToUI(uiModel.eventDate.dateValue) ?: "", textSelection)) + } else { + mutableStateOf(TextFieldValue()) + } } var state by remember { @@ -62,25 +70,21 @@ fun ProvideInputDate( InputDateTime( title = uiModel.eventDate.label ?: "", allowsManualInput = uiModel.allowsManualInput, - value = value, + inputTextFieldValue = value, actionIconType = DateTimeActionIconType.DATE, onActionClicked = uiModel.onDateClick, state = state, visualTransformation = DateTransformation(), onValueChanged = { value = it - state = getInputShellStateBasedOnValue(it) - manageActionBasedOnValue(uiModel, it) + state = getInputShellStateBasedOnValue(it.text) + manageActionBasedOnValue(uiModel, it.text) }, isRequired = uiModel.required, modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), onFocusChanged = { focused -> - if (!focused) { - value?.let { - if (!isValid(it)) { - state = InputShellState.ERROR - } - } + if (!focused && !isValid(value.text)) { + state = InputShellState.ERROR } }, ) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt index 0a820d8993..5bf68ccd12 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt @@ -16,6 +16,7 @@ import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMappe import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel class ProgramEventDetailLiveAdapter( private val program: Program, @@ -65,7 +66,7 @@ class ProgramEventDetailLiveAdapter( ) ListCard( listAvatar = card.avatar, - title = card.title, + title = ListCardTitleModel(text = card.title), lastUpdated = card.lastUpdated, additionalInfoList = card.additionalInfo, actionButton = card.actionButton, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java index a1dc2103a4..cf5675ecd5 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.paging.PagedList; +import androidx.paging.PagingData; import org.dhis2.commons.data.EventViewModel; import org.dhis2.commons.data.SearchTeiModel; @@ -15,6 +16,8 @@ import org.hisp.dhis.android.core.program.Program; import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationsGroup; import org.hisp.dhis.android.core.trackedentity.TrackedEntityType; +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository; +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem; import org.jetbrains.annotations.NotNull; import java.util.Date; @@ -29,9 +32,6 @@ public interface SearchRepository { Observable> programsWithRegistration(String programTypeId); - @NonNull - LiveData> searchTrackedEntities(SearchParametersModel searchParametersModel, boolean isOnline); - void clearFetchedList(); @NonNull @@ -58,6 +58,10 @@ public interface SearchRepository { TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, String reason); + SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem); + + TrackedEntitySearchCollectionRepository getFilteredRepository(SearchParametersModel searchParametersModel); + void setCurrentProgram(@Nullable String currentProgram); boolean programStagesHaveCoordinates(String programUid); boolean teTypeAttributesHaveCoordinates(String typeId); diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index fe74b6c659..2c89d5d569 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -5,9 +5,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; import androidx.paging.DataSource; import androidx.paging.LivePagedListBuilder; import androidx.paging.PagedList; +import androidx.paging.Pager; +import androidx.paging.PagingData; +import androidx.paging.PagingDataTransforms; +import androidx.paging.PagingLiveData; import org.dhis2.R; import org.dhis2.bindings.ExtensionsKt; @@ -97,6 +102,11 @@ import io.reactivex.Flowable; import io.reactivex.Observable; import io.reactivex.Single; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.ExecutorsKt; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.FlowCollector; public class SearchRepositoryImpl implements SearchRepository { @@ -105,7 +115,7 @@ public class SearchRepositoryImpl implements SearchRepository { private final D2 d2; private final SearchSortingValueSetter sortingValueSetter; private TrackedEntitySearchCollectionRepository trackedEntityInstanceQuery; - private SearchParametersModel savedSearchParameters; + public SearchParametersModel savedSearchParameters; private FilterManager savedFilters; private FilterPresenter filterPresenter; private DhisPeriodUtils periodUtils; @@ -168,40 +178,6 @@ public void clearFetchedList() { fetchedTeiUids.clear(); } - @NonNull - @Override - public LiveData> searchTrackedEntities(SearchParametersModel searchParametersModel, boolean isOnline) { - boolean allowCache = false; - if (!searchParametersModel.equals(savedSearchParameters) || !FilterManager.getInstance().sameFilters(savedFilters)) { - trackedEntityInstanceQuery = getFilteredRepository(searchParametersModel); - } else { - getFilteredRepository(searchParametersModel); - allowCache = true; - } - - if (!fetchedTeiUids.isEmpty() && searchParametersModel.getSelectedProgram() == null) { - trackedEntityInstanceQuery = trackedEntityInstanceQuery.excludeUids().in(new ArrayList<>(fetchedTeiUids)); - } - - DataSource dataSource; - - if (isOnline && FilterManager.getInstance().getStateFilters().isEmpty()) { - dataSource = trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst().getResultDataSource() - .map(result -> transformResult(result, searchParametersModel.getSelectedProgram(), false, FilterManager.getInstance().getSortingItem())); - } else { - dataSource = trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly().getResultDataSource() - .map(result -> transformResult(result, searchParametersModel.getSelectedProgram(), true, FilterManager.getInstance().getSortingItem())); - } - - return new LivePagedListBuilder<>(new DataSource.Factory() { - @NonNull - @Override - public DataSource create() { - return dataSource; - } - }, 10).build(); - } - @NonNull @Override public Flowable> searchTeiForMap(SearchParametersModel searchParametersModel, boolean isOnline) { @@ -225,7 +201,8 @@ public Flowable> searchTeiForMap(SearchParametersModel sear .toList().toFlowable(); } - private TrackedEntitySearchCollectionRepository getFilteredRepository(SearchParametersModel searchParametersModel) { + @Override + public TrackedEntitySearchCollectionRepository getFilteredRepository(SearchParametersModel searchParametersModel) { this.savedSearchParameters = searchParametersModel.copy(); this.savedFilters = FilterManager.getInstance().copy(); @@ -247,12 +224,12 @@ private TrackedEntitySearchCollectionRepository getFilteredRepository(SearchPara boolean isUnique = d2.trackedEntityModule().trackedEntityAttributes().uid(dataId).blockingGet().unique(); if (isUnique) { - trackedEntityInstanceQuery = trackedEntityInstanceQuery.byAttribute(dataId).eq(dataValue); + trackedEntityInstanceQuery = trackedEntityInstanceQuery.byFilter(dataId).eq(dataValue); } else if (dataValue.contains("_os_")) { dataValue = dataValue.split("_os_")[1]; - trackedEntityInstanceQuery = trackedEntityInstanceQuery.byAttribute(dataId).eq(dataValue); + trackedEntityInstanceQuery = trackedEntityInstanceQuery.byFilter(dataId).eq(dataValue); } else - trackedEntityInstanceQuery = trackedEntityInstanceQuery.byAttribute(dataId).like(dataValue); + trackedEntityInstanceQuery = trackedEntityInstanceQuery.byFilter(dataId).like(dataValue); } } @@ -725,7 +702,7 @@ public TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, return teiDownloader.download(teiUid, enrollmentUid, reason); } - private SearchTeiModel transformResult(Result result, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { + public SearchTeiModel transformResult(Result result, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { try { return transform(result.getOrThrow(), selectedProgram, offlineOnly, sortingItem); } catch (Exception e) { @@ -736,7 +713,8 @@ private SearchTeiModel transformResult(Result } } - private SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { + @Override + public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Program selectedProgram, boolean offlineOnly, SortingItem sortingItem) { if (!fetchedTeiUids.contains(searchItem.uid())) { fetchedTeiUids.add(searchItem.uid()); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt new file mode 100644 index 0000000000..57987f08d5 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -0,0 +1,50 @@ +package org.dhis2.usescases.searchTrackEntity + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.filters.FilterManager +import org.dhis2.data.search.SearchParametersModel +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem + +class SearchRepositoryImplKt( + private val searchRepositoryJava: SearchRepository, +) : SearchRepositoryKt { + + private lateinit var savedSearchParamenters: SearchParametersModel + + private lateinit var savedFilters: FilterManager + + private lateinit var trackedEntityInstanceQuery: TrackedEntitySearchCollectionRepository + + private val fetchedTeiUids = HashSet() + + override fun searchTrackedEntities( + searchParametersModel: SearchParametersModel, + isOnline: Boolean, + ): Flow> { + var allowCache = false + savedSearchParamenters = searchParametersModel.copy() + savedFilters = FilterManager.getInstance().copy() + + if (searchParametersModel != savedSearchParamenters || !FilterManager.getInstance().sameFilters(savedFilters)) { + trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) + } else { + trackedEntityInstanceQuery = searchRepositoryJava.getFilteredRepository(searchParametersModel) + allowCache = true + } + + if (fetchedTeiUids.isNotEmpty() && searchParametersModel.selectedProgram == null) { + trackedEntityInstanceQuery = + trackedEntityInstanceQuery.excludeUids().`in`(fetchedTeiUids.toList()) + } + + val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isNotEmpty()) { + trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst().getPagingData(10) + } else { + trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly().getPagingData(10) + } + + return pagerFlow + } +} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt new file mode 100644 index 0000000000..382bc6c6a0 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt @@ -0,0 +1,14 @@ +package org.dhis2.usescases.searchTrackEntity + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.data.search.SearchParametersModel +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem + +interface SearchRepositoryKt { + + fun searchTrackedEntities( + searchParametersModel: SearchParametersModel, + isOnline: Boolean, + ): Flow> +} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index fe62d5a57d..c9889afc9e 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -4,11 +4,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.PagedList +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.dhis2.commons.data.SearchTeiModel +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.viewmodel.DispatcherProvider @@ -29,6 +34,7 @@ class SearchTEIViewModel( private val initialProgramUid: String?, initialQuery: MutableMap?, private val searchRepository: SearchRepository, + private val searchRepositoryKt: SearchRepositoryKt, private val searchNavPageConfigurator: SearchPageConfigurator, private val mapDataRepository: MapDataRepository, private val networkUtils: NetworkUtils, @@ -226,11 +232,11 @@ class SearchTEIViewModel( } } - fun fetchListResults(onPagedListReady: (LiveData>?) -> Unit) { + fun fetchListResults(onPagedListReady: (Flow>?) -> Unit) { viewModelScope.launch { val resultPagedList = when { - searching -> loadSearchResults() - displayFrontPageList() -> loadDisplayInListResults() + searching -> loadSearchResults().cachedIn(viewModelScope) + displayFrontPageList() -> loadDisplayInListResults().cachedIn(viewModelScope) else -> null } onPagedListReady(resultPagedList) @@ -238,34 +244,94 @@ class SearchTEIViewModel( } private suspend fun loadSearchResults() = withContext(dispatchers.io()) { - return@withContext searchRepository.searchTrackedEntities( - SearchParametersModel( - selectedProgram = searchRepository.getProgram(initialProgramUid), - queryData = queryData, - ), + val searchParametersModel = SearchParametersModel( + selectedProgram = searchRepository.getProgram(initialProgramUid), + queryData = queryData, + ) + val getPagingData = searchRepositoryKt.searchTrackedEntities( + searchParametersModel, searching && networkUtils.isOnline(), ) + + return@withContext getPagingData.map { pagingData -> + pagingData.map { item -> + if ( + searching && networkUtils.isOnline() && + FilterManager.getInstance().stateFilters.isEmpty() + ) { + searchRepository.transform( + item, + searchParametersModel.selectedProgram, + false, + FilterManager.getInstance().sortingItem, + ) + } else { + searchRepository.transform( + item, + searchParametersModel.selectedProgram, + true, + FilterManager.getInstance().sortingItem, + ) + } + } + } } private suspend fun loadDisplayInListResults() = withContext(dispatchers.io()) { - return@withContext searchRepository.searchTrackedEntities( - SearchParametersModel( - selectedProgram = searchRepository.getProgram(initialProgramUid), - queryData = queryData, - ), + val searchParametersModel = SearchParametersModel( + selectedProgram = searchRepository.getProgram(initialProgramUid), + queryData = queryData, + ) + val getPagingData = searchRepositoryKt.searchTrackedEntities( + searchParametersModel, false, ) + + return@withContext getPagingData.map { pagingData -> + pagingData.map { item -> + searchRepository.transform( + item, + searchParametersModel.selectedProgram, + true, + FilterManager.getInstance().sortingItem, + ) + } + } } - fun fetchGlobalResults(): LiveData>? { - return if (searching) { - searchRepository.searchTrackedEntities( - SearchParametersModel( - selectedProgram = null, - queryData = queryData, - ), - searching && networkUtils.isOnline(), - ) + suspend fun fetchGlobalResults() = withContext(dispatchers.io()) { + val searchParametersModel = SearchParametersModel( + selectedProgram = searchRepository.getProgram(initialProgramUid), + queryData = queryData, + ) + val getPagingData = searchRepositoryKt.searchTrackedEntities( + searchParametersModel, + searching && networkUtils.isOnline(), + ) + + return@withContext if (searching) { + getPagingData.map { pagingData -> + pagingData.map { item -> + if ( + searching && networkUtils.isOnline() && + FilterManager.getInstance().stateFilters.isEmpty() + ) { + searchRepository.transform( + item, + searchParametersModel.selectedProgram, + false, + FilterManager.getInstance().sortingItem, + ) + } else { + searchRepository.transform( + item, + searchParametersModel.selectedProgram, + true, + FilterManager.getInstance().sortingItem, + ) + } + } + } } else { null } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java index 55d1cab699..a6850bd3ab 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java @@ -156,6 +156,14 @@ SearchRepository searchRepository(@NonNull D2 d2, FilterPresenter filterPresente themeManager); } + @Provides + @PerActivity + SearchRepositoryKt searchRepositoryKt( + SearchRepository searchRepository + ) { + return new SearchRepositoryImplKt(searchRepository); + } + @Provides @PerActivity SearchTEIRepository searchTEIRepository(D2 d2) { @@ -248,6 +256,7 @@ FiltersAdapter provideNewFiltersAdapter() { @PerActivity SearchTeiViewModelFactory providesViewModelFactory( SearchRepository searchRepository, + SearchRepositoryKt searchRepositoryKt, MapDataRepository mapDataRepository, NetworkUtils networkUtils, D2 d2, @@ -255,6 +264,7 @@ SearchTeiViewModelFactory providesViewModelFactory( ) { return new SearchTeiViewModelFactory( searchRepository, + searchRepositoryKt, new SearchPageConfigurator(searchRepository), initialProgram, initialQuery, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt index c8bb624e4e..46ff618035 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt @@ -9,6 +9,7 @@ import org.dhis2.maps.usecases.MapStyleConfiguration @Suppress("UNCHECKED_CAST") class SearchTeiViewModelFactory( val searchRepository: SearchRepository, + private val searchRepositoryKt: SearchRepositoryKt, private val searchNavPageConfigurator: SearchPageConfigurator, private val initialProgramUid: String?, private val initialQuery: MutableMap?, @@ -22,6 +23,7 @@ class SearchTeiViewModelFactory( initialProgramUid, initialQuery, searchRepository, + searchRepositoryKt, searchNavPageConfigurator, mapDataRepository, networkUtils, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt index 32ba275c1b..10bb606d35 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt @@ -19,4 +19,12 @@ class SearchAdapterDiffCallback() : DiffUtil.ItemCallback() { oldItem.sortingValue == newItem.sortingValue && oldItem.enrolledOrgUnit == newItem.enrolledOrgUnit } + + override fun getChangePayload(oldItem: SearchTeiModel, newItem: SearchTeiModel): Any? { + return if (oldItem != newItem) { + newItem + } else { + super.getChangePayload(oldItem, newItem) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt index 29e5b86bb3..5eb90556d9 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import org.dhis2.R @@ -14,6 +14,7 @@ import org.dhis2.databinding.ItemSearchErrorBinding import org.dhis2.databinding.ItemSearchTrackedEntityBinding import org.dhis2.usescases.searchTrackEntity.ui.mapper.TEICardMapper import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel class SearchTeiLiveAdapter( private val fromRelationship: Boolean, @@ -28,7 +29,7 @@ class SearchTeiLiveAdapter( private val onDownloadTei: (teiUid: String, enrollmentUid: String?) -> Unit, private val onTeiClick: (teiUid: String, enrollmentUid: String?, isOnline: Boolean) -> Unit, private val onImageClick: (imagePath: String) -> Unit, -) : PagedListAdapter(SearchAdapterDiffCallback()) { +) : PagingDataAdapter(SearchAdapterDiffCallback()) { private enum class SearchItem { TEI, @@ -101,7 +102,7 @@ class SearchTeiLiveAdapter( ) ListCard( listAvatar = card.avatar, - title = card.title, + title = ListCardTitleModel(text = card.title), lastUpdated = card.lastUpdated, additionalInfoList = card.additionalInfo, actionButton = card.actionButton, @@ -134,8 +135,4 @@ class SearchTeiLiveAdapter( is SearchErrorViewHolder -> holder.bind(getItem(position)!!) } } - - fun clearList() { - submitList(null) - } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt index 58d186a0e6..ed3c749044 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt @@ -17,9 +17,11 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.paging.PagedList +import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.dhis2.bindings.dp import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity import org.dhis2.commons.filters.workingLists.WorkingListViewModel @@ -249,17 +251,14 @@ class SearchTEList : FragmentGlobalAbstract() { initLoading(emptyList()) it.firstOrNull()?.let { searchResult -> if (searchResult.shouldClearProgramData()) { - liveAdapter.clearList() + liveAdapter.refresh() } if (searchResult.shouldClearGlobalData()) { - globalAdapter.clearList() + globalAdapter.refresh() } } displayResult(it) updateRecycler() - recycler.post { - recycler.smoothScrollToPosition(0) - } } } @@ -283,51 +282,26 @@ class SearchTEList : FragmentGlobalAbstract() { private fun restoreAdapters() { initLoading(null) - liveAdapter.clearList() + liveAdapter.refresh() if (!viewModel.filtersApplyOnGlobalSearch()) { - globalAdapter.clearList() + globalAdapter.refresh() } else if (globalAdapter.itemCount > 0) { initGlobalData() } displayResult(null) } - private val initResultCallback = object : PagedList.Callback() { - override fun onChanged(position: Int, count: Int) { - } - - override fun onInserted(position: Int, count: Int) { - onInitDataLoaded() - } - - override fun onRemoved(position: Int, count: Int) { - } - } - - private val globalResultCallback = object : PagedList.Callback() { - override fun onChanged(position: Int, count: Int) { - } - - override fun onInserted(position: Int, count: Int) { - onGlobalDataLoaded() - } - - override fun onRemoved(position: Int, count: Int) { - } - } - private fun initData() { displayLoadingData() viewModel.fetchListResults { - it?.takeIf { view != null }?.apply { - removeObservers(viewLifecycleOwner) - observe(viewLifecycleOwner) { results -> - liveAdapter.submitList(results) { + viewModel.viewModelScope.launch { + it?.takeIf { view != null }?.collectLatest { + liveAdapter.addOnPagesUpdatedListener { onInitDataLoaded() } - results.addWeakCallback(results.snapshot(), initResultCallback) - } - } ?: onInitDataLoaded() + liveAdapter.submitData(it) + } ?: onInitDataLoaded() + } } } @@ -340,7 +314,7 @@ class SearchTEList : FragmentGlobalAbstract() { null }, isLandscape = isLandscape(), - onlineErrorCode = liveAdapter.currentList?.lastOrNull()?.onlineErrorCode, + onlineErrorCode = liveAdapter.snapshot().items.lastOrNull()?.onlineErrorCode, ) } @@ -354,13 +328,12 @@ class SearchTEList : FragmentGlobalAbstract() { private fun initGlobalData() { displayLoadingData() - viewModel.fetchGlobalResults()?.let { - it.removeObservers(viewLifecycleOwner) - it.observe(viewLifecycleOwner) { results -> - globalAdapter.submitList(results) { + viewModel.viewModelScope.launch { + viewModel.fetchGlobalResults()?.collectLatest { + globalAdapter.addOnPagesUpdatedListener { onGlobalDataLoaded() } - results.addWeakCallback(results.snapshot(), globalResultCallback) + globalAdapter.submitData(it) } } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java deleted file mode 100644 index 29b2c91e30..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java +++ /dev/null @@ -1,499 +0,0 @@ -package org.dhis2.usescases.teiDashboard; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.PairKt; - -import org.dhis2.R; -import org.dhis2.commons.data.tuples.Pair; -import org.dhis2.commons.data.tuples.Trio; -import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.utils.AuthorityException; -import org.dhis2.utils.DateUtils; -import org.dhis2.utils.ValueUtils; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.arch.helpers.UidsHelper; -import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope; -import org.hisp.dhis.android.core.category.CategoryCombo; -import org.hisp.dhis.android.core.category.CategoryOptionCombo; -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.common.State; -import org.hisp.dhis.android.core.common.ValueType; -import org.hisp.dhis.android.core.enrollment.Enrollment; -import org.hisp.dhis.android.core.enrollment.EnrollmentCollectionRepository; -import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository; -import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; -import org.hisp.dhis.android.core.event.Event; -import org.hisp.dhis.android.core.event.EventStatus; -import org.hisp.dhis.android.core.legendset.Legend; -import org.hisp.dhis.android.core.maintenance.D2Error; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; -import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.android.core.program.ProgramIndicator; -import org.hisp.dhis.android.core.program.ProgramRuleActionType; -import org.hisp.dhis.android.core.program.ProgramStage; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute; -import org.hisp.dhis.android.core.relationship.RelationshipType; -import org.hisp.dhis.android.core.systeminfo.SystemInfo; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityType; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeAttribute; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; - -import dhis2.org.analytics.charts.Charts; -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Single; -import timber.log.Timber; - -public class DashboardRepositoryImpl implements DashboardRepository { - - private final D2 d2; - private final ResourceManager resources; - private final String enrollmentUid; - @Nullable - private final Charts charts; - - private String teiUid; - - private String programUid; - - private TeiAttributesProvider teiAttributesProvider; - - - public DashboardRepositoryImpl(D2 d2, - @Nullable Charts charts, - String teiUid, - String programUid, - String enrollmentUid, - ResourceManager resources, - TeiAttributesProvider teiAttributesProvider) { - this.d2 = d2; - this.teiUid = teiUid; - this.programUid = programUid; - this.enrollmentUid = enrollmentUid; - this.resources = resources; - this.charts = charts; - this.teiAttributesProvider = teiAttributesProvider; - } - - @Override - public Event updateState(Event eventModel, EventStatus newStatus) { - - try { - d2.eventModule().events().uid(eventModel.uid()).setStatus(newStatus); - } catch (D2Error d2Error) { - Timber.e(d2Error); - } - - return d2.eventModule().events().uid(eventModel.uid()).blockingGet(); - } - - @Override - public Observable> getProgramStages(String programUid) { - return d2.programModule().programStages().byProgramUid().eq(programUid).get().toObservable(); - } - - @Override - public Observable getEnrollment() { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).get().toObservable(); - } - - @Override - public Observable> getTEIEnrollmentEvents(String programUid, String teiUid) { - - return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) - .byDeleted().isFalse() - .orderByTimeline(RepositoryScope.OrderByDirection.ASC) - .get().toFlowable().flatMapIterable(events -> events).map(event -> { - if (Boolean.FALSE - .equals(d2.programModule().programs().uid(programUid).blockingGet().ignoreOverdueEvents())) - if (event.status() == EventStatus.SCHEDULE - && event.dueDate().before(DateUtils.getInstance().getToday())) - event = updateState(event, EventStatus.OVERDUE); - - return event; - }).toList() - .toObservable(); - } - - @Override - public Observable> getEnrollmentEventsWithDisplay(String programUid, String teiUid) { - return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).get() - .toObservable() - .map(events -> { - List finalEvents = new ArrayList<>(); - for (Event event : events) { - if (d2.programModule().programStages().uid(event.programStage()).blockingGet().displayGenerateEventBox()) { - finalEvents.add(event); - } - } - return finalEvents; - }); - } - - @Override - public Observable displayGenerateEvent(String eventUid) { - return d2.eventModule().events().uid(eventUid).get().map(Event::programStage) - .flatMap(stageUid -> d2.programModule().programStages().uid(stageUid).get()).toObservable(); - } - - @Override - public Observable>> relationshipsForTeiType(String teType) { - return d2.systemInfoModule().systemInfo().get().toObservable() - .map(SystemInfo::version) - .flatMap(version -> { - if (version.equals("2.29")) - return d2.relationshipModule().relationshipTypes().get().toObservable() - .flatMapIterable(list -> list) - .map(relationshipType -> Pair.create(relationshipType, teType)).toList().toObservable(); - else - return d2.relationshipModule().relationshipTypes().withConstraints().get() - .map(relationshipTypes -> { - List> relTypeList = new ArrayList<>(); - for (RelationshipType relationshipType : relationshipTypes) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint().trackedEntityType() != null && - relationshipType.fromConstraint().trackedEntityType().uid().equals(teType)) { - if (relationshipType.toConstraint() != null && relationshipType.toConstraint().trackedEntityType() != null) { - relTypeList.add(Pair.create(relationshipType, relationshipType.toConstraint().trackedEntityType().uid())); - } - } else if (relationshipType.bidirectional() && relationshipType.toConstraint() != null && relationshipType.toConstraint().trackedEntityType() != null && - relationshipType.toConstraint().trackedEntityType().uid().equals(teType)) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint().trackedEntityType() != null) { - relTypeList.add(Pair.create(relationshipType, relationshipType.fromConstraint().trackedEntityType().uid())); - } - } - } - return relTypeList; - }).toObservable(); - }); - } - - @Override - public CategoryOptionCombo catOptionCombo(String catComboUid) { - return d2.categoryModule().categoryOptionCombos().uid(catComboUid).blockingGet(); - } - - public Observable>> getAttributesMap(String programUid, String teiUid) { - return teiAttributesProvider.getProgramTrackedEntityAttributesByProgram(programUid, teiUid) - .toObservable() - .flatMapIterable(list -> list) - .map(pair -> { - TrackedEntityAttribute attribute = pair.getFirst(); - TrackedEntityAttributeValue attributeValue = pair.getSecond(); - - TrackedEntityAttributeValue formattedAttributeValue; - - if (attributeValue != null && attribute.valueType() != ValueType.IMAGE) { - formattedAttributeValue = ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null); - } else { - formattedAttributeValue = TrackedEntityAttributeValue.builder() - .trackedEntityAttribute(attribute.uid()) - .trackedEntityInstance(teiUid) - .value("") - .build(); - } - return Pair.create(attribute, formattedAttributeValue); - }).toList().toObservable(); - } - - @Override - public Observable> getTEIAttributeValues(String programUid, String teiUid) { - if (programUid != null) { - return teiAttributesProvider.getValuesFromProgramTrackedEntityAttributesByProgram(programUid, teiUid) - .map(attributesValues -> { - List formattedValues = new ArrayList<>(); - for (TrackedEntityAttributeValue attributeValue : attributesValues) { - if (attributeValue.value() != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - if (attribute.valueType() != ValueType.IMAGE) { - formattedValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } else { - formattedValues.add( - TrackedEntityAttributeValue.builder() - .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) - .trackedEntityInstance(teiUid) - .value("") - .build() - ); - } - } - return formattedValues; - }).toObservable(); - - } else { - String teType = d2.trackedEntityModule().trackedEntityInstances().uid(teiUid).blockingGet().trackedEntityType(); - List attributeValues = new ArrayList<>(); - - for (TrackedEntityAttributeValue attributeValue: teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes(teType, teiUid)) { - if (attributeValue != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - if (attribute.valueType() != ValueType.IMAGE && attributeValue.value() != null) { - attributeValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } - } - - if (attributeValues.isEmpty()) { - for (TrackedEntityAttributeValue attributeValue: teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes(teType, teiUid)) { - if (attributeValue != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - attributeValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } - } - return Observable.just(attributeValues); - } - } - - @Override - public boolean setFollowUp(String enrollmentUid) { - - boolean followUp = Boolean.TRUE - .equals(d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().followUp()); - try { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(!followUp); - return !followUp; - } catch (D2Error d2Error) { - Timber.e(d2Error); - return followUp; - } - } - - @Override - public Flowable completeEnrollment(@NonNull String enrollmentUid) { - return Flowable.fromCallable(() -> { - d2.enrollmentModule().enrollments().uid(enrollmentUid) - .setStatus(EnrollmentStatus.COMPLETED); - return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet(); - }); - } - - @Override - public Observable getTrackedEntityInstance(String teiUid) { - return Observable.fromCallable( - () -> d2.trackedEntityModule().trackedEntityInstances().byUid().eq(teiUid).one().blockingGet()); - } - - @Override - public Observable> getProgramTrackedEntityAttributes(String programUid) { - if (programUid != null) { - return d2.programModule().programTrackedEntityAttributes().byProgram().eq(programUid) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).get().toObservable(); - } else { - return Observable.fromCallable(() -> d2.trackedEntityModule().trackedEntityAttributes() - .byDisplayInListNoProgram().eq(true).blockingGet()).map(trackedEntityAttributes -> { - List programs = d2.programModule().programs().blockingGet(); - - List teaUids = UidsHelper.getUidsList(trackedEntityAttributes); - List programTrackedEntityAttributes = new ArrayList<>(); - - for (Program program : programs) { - List attributeList = d2.programModule() - .programTrackedEntityAttributes().byProgram().eq(program.uid()) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet(); - - for (ProgramTrackedEntityAttribute pteattr : attributeList) { - if (teaUids.contains(pteattr.uid())) - programTrackedEntityAttributes.add(pteattr); - } - } - return programTrackedEntityAttributes; - }); - } - } - - @Override - public Observable> getTeiOrgUnits(@NonNull String teiUid, @Nullable String programUid) { - EnrollmentCollectionRepository enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() - .eq(teiUid); - if (programUid != null) { - enrollmentRepo = enrollmentRepo.byProgram().eq(programUid); - } - - return enrollmentRepo.get().toObservable().map(enrollments -> { - List orgUnitIds = new ArrayList<>(); - for (Enrollment enrollment : enrollments) { - orgUnitIds.add(enrollment.organisationUnit()); - } - return d2.organisationUnitModule().organisationUnits().byUid().in(orgUnitIds).blockingGet(); - }); - } - - @Override - public Observable> getTeiActivePrograms(String teiUid, boolean showOnlyActive) { - EnrollmentCollectionRepository enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() - .eq(teiUid).byDeleted().eq(false); - if (showOnlyActive) - enrollmentRepo.byStatus().eq(EnrollmentStatus.ACTIVE); - return enrollmentRepo.get().toObservable().flatMapIterable(enrollments -> enrollments) - .map(Enrollment::program).toList().toObservable() - .map(programUids -> d2.programModule().programs().byUid().in(programUids).blockingGet()); - } - - @Override - public Observable> getTEIEnrollments(String teiUid) { - return d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid).byDeleted().eq(false).get().toObservable(); - } - - @Override - public void saveCatOption(String eventUid, String catOptionComboUid) { - try { - d2.eventModule().events().uid(eventUid).setAttributeOptionComboUid(catOptionComboUid); - } catch (D2Error d2Error) { - Timber.e(d2Error); - } - } - - @Override - public Single deleteTeiIfPossible() { - return Single.fromCallable(() -> { - boolean local = d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .blockingGet() - .state() == State.TO_POST; - boolean hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_TEI_CASCADE_DELETE") - .one().blockingExists(); - return local || hasAuthority; - }).flatMap(canDelete -> { - if (canDelete) { - return d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .delete() - .andThen(Single.fromCallable(() -> true)); - } else { - return Single.fromCallable(() -> false); - } - }); - } - - @Override - public Single deleteEnrollmentIfPossible(String enrollmentUid) { - return Single.fromCallable(() -> { - boolean local = d2.enrollmentModule() - .enrollments() - .uid(enrollmentUid) - .blockingGet().state() == State.TO_POST; - boolean hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_ENROLLMENT_CASCADE_DELETE") - .one().blockingExists(); - return local || hasAuthority; - }).flatMap(canDelete -> { - if (canDelete) { - return Single.fromCallable(() -> { - EnrollmentObjectRepository enrollmentObjectRepository = d2.enrollmentModule() - .enrollments().uid(enrollmentUid); - enrollmentObjectRepository.setStatus( - enrollmentObjectRepository.blockingGet().status() - ); - enrollmentObjectRepository.blockingDelete(); - return !d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) - .byDeleted().isFalse() - .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isEmpty(); - }); - } else { - return Single.error(new AuthorityException(null)); - } - }); - } - - @Override - public Single getNoteCount() { - return d2.enrollmentModule().enrollments() - .withNotes() - .uid(enrollmentUid) - .get() - .map(enrollment -> enrollment.notes() != null ? enrollment.notes().size() : 0); - } - - @Override - public EnrollmentStatus getEnrollmentStatus(String enrollmentUid) { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().status(); - } - - @Override - public Observable updateEnrollmentStatus(String enrollmentUid, EnrollmentStatus status) { - try { - if (d2.programModule().programs().uid(programUid).blockingGet().access().data().write()) { - if (reopenCheck(status)) { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setStatus(status); - return Observable.just(StatusChangeResultCode.CHANGED); - } else { - return Observable.just(StatusChangeResultCode.ACTIVE_EXIST); - } - } else { - return Observable.just(StatusChangeResultCode.WRITE_PERMISSION_FAIL); - } - } catch (D2Error error) { - return Observable.just(StatusChangeResultCode.FAILED); - } - } - - private boolean reopenCheck(EnrollmentStatus status) { - return status != EnrollmentStatus.ACTIVE || d2.enrollmentModule().enrollments() - .byProgram().eq(programUid) - .byTrackedEntityInstance().eq(teiUid) - .byStatus().eq(EnrollmentStatus.ACTIVE) - .blockingIsEmpty(); - } - - @Override - public boolean programHasRelationships() { - if (programUid != null) { - String teiTypeUid = d2.programModule().programs() - .uid(programUid) - .blockingGet() - .trackedEntityType() - .uid(); - return !relationshipsForTeiType(teiTypeUid).blockingFirst().isEmpty(); - } else { - return false; - } - } - - @Override - public boolean programHasAnalytics() { - if (programUid != null) { - List enrollmentScopeRulesUids = d2.programModule().programRules() - .byProgramUid().eq(programUid) - .byProgramStageUid().isNull() - .blockingGetUids(); - boolean hasDisplayRuleActions = !d2.programModule().programRuleActions() - .byProgramRuleUid().in(enrollmentScopeRulesUids) - .byProgramRuleActionType().in(ProgramRuleActionType.DISPLAYKEYVALUEPAIR, ProgramRuleActionType.DISPLAYTEXT) - .blockingIsEmpty(); - boolean hasProgramIndicator = !d2.programModule().programIndicators().byProgramUid().eq(programUid).blockingIsEmpty(); - boolean hasCharts = charts != null && !charts.geEnrollmentCharts(enrollmentUid).isEmpty(); - return hasDisplayRuleActions || hasProgramIndicator || hasCharts; - } else { - return false; - } - } - - @Override - public String getTETypeName() { - return getTrackedEntityInstance(teiUid).flatMap(tei -> - d2.trackedEntityModule().trackedEntityTypes() - .uid(tei.trackedEntityType()) - .get() - .toObservable() - ).blockingFirst().displayName(); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt new file mode 100644 index 0000000000..30ea10c103 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -0,0 +1,562 @@ +package org.dhis2.usescases.teiDashboard + +import dhis2.org.analytics.charts.Charts +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Function +import org.dhis2.commons.data.tuples.Pair +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.utils.AuthorityException +import org.dhis2.utils.DateUtils +import org.dhis2.utils.ValueUtils +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.category.CategoryOptionCombo +import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramRuleActionType +import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute +import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.android.core.systeminfo.SystemInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import timber.log.Timber + +class DashboardRepositoryImpl( + private val d2: D2, + private val charts: Charts, + private val teiUid: String, + private val programUid: String, + private val enrollmentUid: String, + private val resources: ResourceManager, + private val teiAttributesProvider: TeiAttributesProvider, +) : DashboardRepository { + override fun getProgramStages(programStages: String): Observable> { + return d2.programModule().programStages().byProgramUid().eq(programUid).get().toObservable() + } + + override fun getEnrollment(): Observable { + return d2.enrollmentModule().enrollments().uid(enrollmentUid).get().map { it }.toObservable() + } + + override fun getTEIEnrollmentEvents( + programUid: String?, + teiUid: String, + ): Observable> { + return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) + .byDeleted().isFalse + .orderByTimeline(RepositoryScope.OrderByDirection.ASC) + .get().toFlowable().flatMapIterable { events: List? -> events } + .map { event: Event -> + var event = event + if (java.lang.Boolean.FALSE + == d2.programModule().programs().uid(programUid).blockingGet()!! + .ignoreOverdueEvents() + ) if (event.status() == EventStatus.SCHEDULE && + event.dueDate()!! + .before(DateUtils.getInstance().today) + ) { + event = updateState(event, EventStatus.OVERDUE) + } + event + }.toList() + .toObservable() + } + + override fun getEnrollmentEventsWithDisplay( + programUid: String?, + teiUid: String, + ): Observable> { + return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).get() + .toObservable() + .map { events: List -> + val finalEvents: MutableList = + ArrayList() + for (event in events) { + if (d2.programModule().programStages().uid(event.programStage()).blockingGet()!! + .displayGenerateEventBox()!! + ) { + finalEvents.add(event) + } + } + finalEvents + } + } + + override fun getTEIAttributeValues( + programUid: String?, + teiUid: String, + ): Observable> { + return if (programUid != null) { + teiAttributesProvider.getValuesFromProgramTrackedEntityAttributesByProgram( + programUid, + teiUid, + ) + .map> { attributesValues: List -> + val formattedValues: MutableList = + java.util.ArrayList() + for (attributeValue in attributesValues) { + if (attributeValue.value() != null) { + val attribute = + d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE) { + formattedValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute!!.valueType(), + if (attribute!!.optionSet() != null) { + attribute!!.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } else { + formattedValues.add( + TrackedEntityAttributeValue.builder() + .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) + .trackedEntityInstance(teiUid) + .value("") + .build(), + ) + } + } + formattedValues + }.toObservable() + } else { + val teType = + d2.trackedEntityModule().trackedEntityInstances().uid(teiUid).blockingGet()!! + .trackedEntityType() + val attributeValues: MutableList = java.util.ArrayList() + for (attributeValue in teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes( + teType, + teiUid, + )) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE && attributeValue.value() != null) { + attributeValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } + if (attributeValues.isEmpty()) { + for (attributeValue in teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes( + teType, + teiUid, + )) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + attributeValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute!!.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } + Observable.just(attributeValues) + } + } + + override fun setFollowUp(enrollmentUid: String?): Boolean { + val followUp = ( + java.lang.Boolean.TRUE + == d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! + .followUp() + ) + return try { + d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(!followUp) + !followUp + } catch (d2Error: D2Error) { + Timber.e(d2Error) + followUp + } + } + + override fun updateState(event: Event?, newStatus: EventStatus): Event { + try { + d2.eventModule().events().uid(event?.uid()).setStatus(newStatus) + } catch (d2Error: D2Error) { + Timber.e(d2Error) + } + return d2.eventModule().events().uid(event?.uid()).blockingGet()!! + } + + override fun completeEnrollment(enrollmentUid: String): Flowable { + return Flowable.fromCallable { + d2.enrollmentModule().enrollments().uid(enrollmentUid) + .setStatus(EnrollmentStatus.COMPLETED) + d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet() + } + } + + override fun displayGenerateEvent(eventUid: String?): Observable { + return d2.eventModule().events().uid(eventUid).get() + .map { obj: Event -> obj.programStage() } + .flatMap { stageUid: String -> + d2.programModule().programStages().uid(stageUid).get() + }.map { it }.toObservable() + } + + override fun relationshipsForTeiType(teType: String): Observable>> { + return d2.systemInfoModule().systemInfo().get().toObservable() + .map(Function { obj: SystemInfo -> obj.version() }) + .flatMap { version: String? -> + if (version == "2.29") { + return@flatMap d2.relationshipModule().relationshipTypes() + .get().toObservable() + .flatMapIterable { list: List? -> list } + .map> { relationshipType: RelationshipType? -> + Pair.create( + relationshipType!!, + teType, + ) + }.toList().toObservable() + } else { + return@flatMap d2.relationshipModule() + .relationshipTypes().withConstraints().get() + .map>> { relationshipTypes: List -> + val relTypeList: MutableList> = + java.util.ArrayList() + for (relationshipType in relationshipTypes) { + if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! + .trackedEntityType() != null && relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid() == teType + ) { + if (relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.toConstraint()!! + .trackedEntityType()!!.uid(), + ), + ) + } + } else if (relationshipType.bidirectional()!! && relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null && relationshipType.toConstraint()!! + .trackedEntityType()!! + .uid() == teType + ) { + if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! + .trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid(), + ), + ) + } + } + } + relTypeList.toList() + }.toObservable() + } + } + } + + override fun catOptionCombo(catComboUid: String?): CategoryOptionCombo { + return d2.categoryModule().categoryOptionCombos().uid(catComboUid).blockingGet()!! + } + + override fun getTrackedEntityInstance(teiUid: String): Observable { + return Observable.fromCallable { + d2.trackedEntityModule().trackedEntityInstances().byUid().eq(teiUid).one() + .blockingGet() + } + } + + override fun getProgramTrackedEntityAttributes(programUid: String?): Observable> { + return if (programUid != null) { + d2.programModule().programTrackedEntityAttributes().byProgram().eq(programUid) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).get().toObservable() + } else { + Observable.fromCallable { + d2.trackedEntityModule().trackedEntityAttributes() + .byDisplayInListNoProgram().eq(true).blockingGet() + }.map { trackedEntityAttributes: List -> + val programs = + d2.programModule().programs().blockingGet() + val teaUids = getUidsList(trackedEntityAttributes) + val programTrackedEntityAttributes: MutableList = + java.util.ArrayList() + for (program in programs) { + val attributeList = + d2.programModule() + .programTrackedEntityAttributes().byProgram().eq(program.uid()) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet() + for (pteattr in attributeList) { + if (teaUids.contains(pteattr.uid())) { + programTrackedEntityAttributes.add( + pteattr, + ) + } + } + } + programTrackedEntityAttributes + } + } + } + + override fun getTeiOrgUnits( + teiUid: String, + programUid: String?, + ): Observable> { + var enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() + .eq(teiUid) + if (programUid != null) { + enrollmentRepo = enrollmentRepo.byProgram().eq(programUid) + } + + return enrollmentRepo.get().toObservable().map { enrollments: List -> + val orgUnitIds: MutableList = + java.util.ArrayList() + for (enrollment in enrollments) { + enrollment.organisationUnit()?.let { orgUnitIds.add(it) } + } + d2.organisationUnitModule().organisationUnits().byUid().`in`(orgUnitIds.toList()) + .blockingGet() + } + } + + override fun getTeiActivePrograms( + teiUid: String, + showOnlyActive: Boolean, + ): Observable> { + val enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() + .eq(teiUid).byDeleted().eq(false) + if (showOnlyActive) enrollmentRepo.byStatus().eq(EnrollmentStatus.ACTIVE) + + return enrollmentRepo.get().toObservable() + .flatMapIterable { enrollments: List? -> enrollments } + .map { obj: Enrollment -> obj.program() } + .toList().toObservable() + .map { programUids -> + d2.programModule().programs().byUid() + .`in`(programUids.filterNotNull()).blockingGet() + } + } + + override fun getTEIEnrollments(teiUid: String): Observable> { + return d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid).byDeleted() + .eq(false).get().toObservable() + } + + override fun saveCatOption(eventUid: String?, catOptionComboUid: String?) { + try { + d2.eventModule().events().uid(eventUid).setAttributeOptionComboUid(catOptionComboUid) + } catch (d2Error: D2Error) { + Timber.e(d2Error) + } + } + + override fun deleteTeiIfPossible(): Single { + return Single.fromCallable { + val local = d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .blockingGet() + ?.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_TEI_CASCADE_DELETE") + .one().blockingExists() + local || hasAuthority + }.flatMap { canDelete: Boolean -> + if (canDelete) { + return@flatMap d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .delete() + .andThen(Single.fromCallable { true }) + } else { + return@flatMap Single.fromCallable { false } + } + } + } + + override fun deleteEnrollmentIfPossible(enrollmentUid: String): Single { + return Single.fromCallable { + val local = d2.enrollmentModule() + .enrollments() + .uid(enrollmentUid) + .blockingGet()!!.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_ENROLLMENT_CASCADE_DELETE") + .one().blockingExists() + local || hasAuthority + }.flatMap { canDelete: Boolean -> + if (canDelete) { + return@flatMap Single.fromCallable { + val enrollmentObjectRepository = d2.enrollmentModule() + .enrollments().uid(enrollmentUid) + enrollmentObjectRepository.setStatus( + enrollmentObjectRepository.blockingGet()!!.status()!!, + ) + enrollmentObjectRepository.blockingDelete() + d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) + .byDeleted().isFalse + .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isNotEmpty() + } + } else { + return@flatMap Single.error(AuthorityException(null)) + } + } + } + + override fun getNoteCount(): Single { + return d2.enrollmentModule().enrollments() + .withNotes() + .uid(enrollmentUid) + .get() + .map(Function { enrollment: Enrollment -> if (enrollment.notes() != null) enrollment.notes()!!.size else 0 }) + } + + override fun getEnrollmentStatus(enrollmentUid: String?): EnrollmentStatus? { + return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!!.status() + } + + override fun updateEnrollmentStatus( + enrollmentUid: String, + status: EnrollmentStatus, + ): Observable { + return try { + if (d2.programModule().programs().uid(programUid).blockingGet()!!.access().data() + .write() + ) { + if (reopenCheck(status)) { + d2.enrollmentModule().enrollments().uid(enrollmentUid).setStatus(status) + Observable.just(StatusChangeResultCode.CHANGED) + } else { + Observable.just(StatusChangeResultCode.ACTIVE_EXIST) + } + } else { + Observable.just(StatusChangeResultCode.WRITE_PERMISSION_FAIL) + } + } catch (error: D2Error) { + Observable.just(StatusChangeResultCode.FAILED) + } + } + + private fun reopenCheck(status: EnrollmentStatus): Boolean { + return status != EnrollmentStatus.ACTIVE || d2.enrollmentModule().enrollments() + .byProgram().eq(programUid) + .byTrackedEntityInstance().eq(teiUid) + .byStatus().eq(EnrollmentStatus.ACTIVE) + .blockingIsEmpty() + } + + override fun programHasRelationships(): Boolean { + return if (programUid != null) { + val teiTypeUid = d2.programModule().programs() + .uid(programUid) + .blockingGet() + ?.trackedEntityType() + ?.uid() + teiTypeUid?.let { relationshipsForTeiType(it) }!!.blockingFirst().isNotEmpty() + } else { + false + } + } + + override fun programHasAnalytics(): Boolean { + return if (programUid != null) { + val enrollmentScopeRulesUids = d2.programModule().programRules() + .byProgramUid().eq(programUid) + .byProgramStageUid().isNull + .blockingGetUids() + val hasDisplayRuleActions = !d2.programModule().programRuleActions() + .byProgramRuleUid().`in`(enrollmentScopeRulesUids) + .byProgramRuleActionType() + .`in`(ProgramRuleActionType.DISPLAYKEYVALUEPAIR, ProgramRuleActionType.DISPLAYTEXT) + .blockingIsEmpty() + val hasProgramIndicator = + !d2.programModule().programIndicators().byProgramUid().eq(programUid) + .blockingIsEmpty() + val hasCharts = charts.geEnrollmentCharts(enrollmentUid).isNotEmpty() + hasDisplayRuleActions || hasProgramIndicator || hasCharts + } else { + false + } + } + + override fun getTETypeName(): String? { + return getTrackedEntityInstance(teiUid).flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + }.blockingFirst()?.displayName() + } + + override fun getAttributesMap( + programUid: String, + teiUid: String, + ): Observable>> { + return teiAttributesProvider.getProgramTrackedEntityAttributesByProgram(programUid, teiUid) + .toObservable() + .flatMapIterable { list: List>? -> list } + .map { (attribute, attributeValue): kotlin.Pair -> + val formattedAttributeValue: TrackedEntityAttributeValue = if (attributeValue != null && attribute!!.valueType() != ValueType.IMAGE) { + ValueUtils.transform( + d2, + attributeValue, + attribute.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ) + } else { + TrackedEntityAttributeValue.builder() + .trackedEntityAttribute(attribute!!.uid()) + .trackedEntityInstance(teiUid) + .value("") + .build() + } + Pair.create( + attribute, + formattedAttributeValue, + ) + }.toList().toObservable() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt index fda6a377e7..b6ddd5bcde 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -145,9 +145,9 @@ class TeiDashboardMobileActivity : (applicationContext as App).createDashboardComponent( TeiDashboardModule( this, - teiUid, - programUid, - enrollmentUid, + teiUid ?: "", + programUid ?: "", + enrollmentUid ?: "", this.isPortrait(), ), ).inject(this) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt index 35109ba13d..da61839c2e 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt @@ -20,9 +20,9 @@ import org.hisp.dhis.android.core.D2 @Module class TeiDashboardModule( private val view: TeiDashboardContracts.View, - val teiUid: String?, - val programUid: String?, - private val enrollmentUid: String?, + val teiUid: String, + val programUid: String, + private val enrollmentUid: String, private val isPortrait: Boolean, ) { @Provides diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt index 0daf8d62be..a9ba0d848f 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt @@ -12,6 +12,12 @@ import org.dhis2.utils.dialFloatingActionButton.DialItem class EventCreationOptionsMapper(val resources: ResourceManager) { + companion object { + const val REFERAL_ID = 3 + const val ADD_NEW_ID = 2 + const val SCHEDULE_ID = 1 + } + fun mapToEventsByStage(availableOptions: List): List { return availableOptions.map { item -> EventCreationOptions( @@ -40,12 +46,25 @@ class EventCreationOptionsMapper(val resources: ResourceManager) { } } + fun getActionType(eventCreationId: Int): EventCreationType { + return when (eventCreationId) { + SCHEDULE_ID -> SCHEDULE + ADD_NEW_ID -> ADDNEW + REFERAL_ID -> REFERAL + else -> throw UnsupportedOperationException( + "id %s is not supported as an event creation".format( + eventCreationId, + ), + ) + } + } + private fun getItemId(item: EventCreationType): Int { return when (item) { - SCHEDULE -> TEIDataFragment.SCHEDULE_ID - ADDNEW -> TEIDataFragment.ADD_NEW_ID - REFERAL -> TEIDataFragment.REFERAL_ID - DEFAULT -> TEIDataFragment.ADD_NEW_ID + SCHEDULE -> SCHEDULE_ID + ADDNEW -> ADD_NEW_ID + REFERAL -> REFERAL_ID + DEFAULT -> ADD_NEW_ID } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt index ea2116b6fa..3a81eb28c7 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata import android.content.Intent import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LifecycleOwner import io.reactivex.Flowable import io.reactivex.Single import io.reactivex.functions.Consumer @@ -20,14 +21,14 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance class TEIDataContracts { interface View : AbstractActivityContracts.View { - fun setEvents(events: List, canAddEvents: Boolean) + fun viewLifecycleOwner(): LifecycleOwner + fun setEvents(events: List) fun displayGenerateEvent(): Consumer fun areEventsCompleted(): Consumer> fun enrollmentCompleted(): Consumer fun switchFollowUp(followUp: Boolean) fun displayGenerateEvent(eventUid: String) fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) - fun seeDetails(intent: Intent, options: ActivityOptionsCompat) fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) fun openEventInitial(intent: Intent) fun openEventCapture(intent: Intent) @@ -50,7 +51,7 @@ class TEIDataContracts { fun showSyncDialog(eventUid: String, enrollmentUid: String) fun displayCatComboOptionSelectorForEvents(data: List) - fun showProgramRuleErrorMessage(message: String) + fun showProgramRuleErrorMessage() fun showCatOptComboDialog(catComboUid: String) fun goToEventInitial(eventCreationType: EventCreationType, programStage: ProgramStage) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt index 22e458dbf3..145ef18362 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt @@ -8,12 +8,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.app.ActivityOptionsCompat import androidx.databinding.ObservableBoolean import androidx.fragment.app.activityViewModels +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DividerItemDecoration import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.CircleCrop @@ -93,6 +93,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { @Inject lateinit var infoBarMapper: InfoBarMapper + @Inject + lateinit var contractHandler: TeiDataContractHandler + private var eventAdapter: EventAdapter? = null private var dialog: CustomDialog? = null private var programStageFromEvent: ProgramStage? = null @@ -102,29 +105,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { private lateinit var dashboardModel: DashboardProgramModel private val dashboardActivity: TeiDashboardMobileActivity by lazy { context as TeiDashboardMobileActivity } - private val detailsLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { - dashboardActivity.presenter.init() - } - - private val eventCreationLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventCaptureLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventDetailsLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventInitialLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - override fun onAttach(context: Context) { super.onAttach(context) with(requireArguments()) { @@ -138,6 +118,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { programUid, teiUid, enrollmentUid, + requireActivity().activityResultRegistry, ), )?.inject(this@TEIDataFragment) } @@ -153,13 +134,19 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { binding.presenter = presenter dashboardActivity.observeGrouping()?.observe(viewLifecycleOwner) { group -> showLoadingProgress(true) - binding.isGrouping = group presenter.onGroupingChanged(group) } dashboardActivity.observeFilters()?.observe(viewLifecycleOwner, ::showHideFilters) dashboardActivity.updatedEnrollment()?.observe(viewLifecycleOwner, ::updateEnrollment) binding.filterLayout.adapter = filtersAdapter + presenter.shouldDisplayEventCreationButton.observe(this.viewLifecycleOwner) { showCreateEventButton -> + binding.dialFabLayout.visibility = if (showCreateEventButton) { + View.VISIBLE.also { binding.dialFabLayout.setFabVisible(true) } + } else { + View.GONE.also { binding.dialFabLayout.setFabVisible(false) } + } + } }.root } @@ -169,14 +156,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { private fun updateFabItems() { val dialItems = presenter.newEventOptionsByTimeline() - binding.dialFabLayout.addDialItems(dialItems) { clickedId: Int? -> - when (clickedId) { - REFERAL_ID -> createEvent(EventCreationType.REFERAL, 0) - ADD_NEW_ID -> createEvent(EventCreationType.ADDNEW, 0) - SCHEDULE_ID -> createEvent(EventCreationType.SCHEDULE, 0) - else -> {} - } - } + binding.dialFabLayout.addDialItems(dialItems, presenter::onEventCreationClick) } override fun setEnrollment(enrollment: Enrollment) { @@ -228,7 +208,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { fun setData(dashboardModel: DashboardProgramModel) { this.dashboardModel = dashboardModel if (dashboardModel.currentEnrollment != null) { - binding.dialFabLayout.setFabVisible(true) +// binding.dialFabLayout.setFabVisible(true) presenter.setDashboardProgram(dashboardModel) eventCatComboOptionSelector = EventCatComboOptionSelector( dashboardModel.currentProgram.categoryComboUid(), @@ -298,7 +278,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { ) } } else { - binding.dialFabLayout.setFabVisible(false) +// binding.dialFabLayout.setFabVisible(false) binding.teiRecycler.adapter = DashboardProgramAdapter(presenter, dashboardModel) binding.teiRecycler.addItemDecoration( DividerItemDecoration( @@ -327,6 +307,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { Intent.ACTION_DIAL -> { data = Uri.parse("tel:$value") } + Intent.ACTION_SENDTO -> { data = Uri.parse("mailto:$value") } @@ -359,8 +340,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { return eventAdapter?.stageSelector() ?: Flowable.empty() } - override fun setEvents(events: List, canAddEvents: Boolean) { - binding.canAddEvents = canAddEvents + override fun setEvents(events: List) { if (events.isEmpty()) { binding.emptyTeis.visibility = View.VISIBLE if (binding.dialFabLayout.isFabVisible()) { @@ -401,9 +381,8 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { RC_GENERATE_EVENT, object : DialogClickListener { override fun onPositive() { - createEvent( - EventCreationType.SCHEDULE, - if (programStageFromEvent?.standardInterval() != null) programStageFromEvent?.standardInterval() else 0, + presenter.onAcceptScheduleNewEvent( + programStageModel.standardInterval() ?: 0, ) } @@ -484,10 +463,16 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, scheduleIntervalDays ?: 0) val intent = Intent(context, ProgramStageSelectionActivity::class.java) intent.putExtras(bundle) - eventCreationLauncher.launch(intent) + contractHandler.createEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } } } + override fun viewLifecycleOwner(): LifecycleOwner { + return this.viewLifecycleOwner + } + override fun switchFollowUp(followUp: Boolean) { this.followUp.set(followUp) } @@ -509,15 +494,20 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { dashboardActivity.finish() } - override fun seeDetails(intent: Intent, options: ActivityOptionsCompat) = - detailsLauncher.launch(intent, options) - override fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) = - eventDetailsLauncher.launch(intent, options) + contractHandler.scheduleEvent(intent, options).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } - override fun openEventInitial(intent: Intent) = eventInitialLauncher.launch(intent) + override fun openEventInitial(intent: Intent) = + contractHandler.editEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } - override fun openEventCapture(intent: Intent) = eventCaptureLauncher.launch(intent) + override fun openEventCapture(intent: Intent) = + contractHandler.editEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } override fun showTeiImage(filePath: String, defaultIcon: String) { if (filePath.isEmpty() && defaultIcon.isEmpty()) { @@ -571,7 +561,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { bundle.putString(Constants.PROGRAM_STAGE_UID, programStage.uid()) bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, programStage.standardInterval() ?: 0) intent.putExtras(bundle) - eventInitialLauncher.launch(intent) + contractHandler.createEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } } private fun showHideFilters(showFilters: Boolean) { @@ -635,8 +627,10 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { eventCatComboOptionSelector?.requestCatComboOption(presenter::changeCatOption) } - override fun showProgramRuleErrorMessage(message: String) { - dashboardActivity.runOnUiThread { showDescription(message) } + override fun showProgramRuleErrorMessage() { + dashboardActivity.runOnUiThread { + showDescription(getString(R.string.error_applying_rule_effects)) + } } override fun showCatOptComboDialog(catComboUid: String) { @@ -656,9 +650,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { companion object { const val RC_GENERATE_EVENT = 1501 const val RC_EVENTS_COMPLETED = 1601 - const val REFERAL_ID = 3 - const val ADD_NEW_ID = 2 - const val SCHEDULE_ID = 1 const val PREF_COMPLETED_EVENT = "COMPLETED_EVENT" @JvmStatic diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt index 4c601f9be7..2dcb1d19d6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt @@ -1,5 +1,6 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata +import androidx.activity.result.ActivityResultRegistry import dagger.Module import dagger.Provides import org.dhis2.commons.data.EntryMode @@ -21,6 +22,7 @@ import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts import org.dhis2.usescases.teiDashboard.data.ProgramConfigurationRepository import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper @@ -34,6 +36,7 @@ class TEIDataModule( private val programUid: String?, private val teiUid: String, private val enrollmentUid: String, + private val registry: ActivityResultRegistry, ) { @Provides @PerFragment @@ -48,10 +51,11 @@ class TEIDataModule( filterManager: FilterManager, filterRepository: FilterRepository, valueStore: FormValueStore, - resources: ResourceManager, optionsRepository: OptionsRepository, getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions, eventCreationOptionsMapper: EventCreationOptionsMapper, + contractHandler: TeiDataContractHandler, + dashboardActivityPresenter: TeiDashboardContracts.Presenter, ): TEIDataPresenter { return TEIDataPresenter( view, @@ -68,10 +72,11 @@ class TEIDataModule( filterManager, filterRepository, valueStore, - resources, optionsRepository, getNewEventCreationTypeOptions, eventCreationOptionsMapper, + contractHandler, + dashboardActivityPresenter, ) } @@ -152,4 +157,7 @@ class TEIDataModule( ): InfoBarMapper { return InfoBarMapper(resourceManager) } + + @Provides + fun provideContractHandler() = TeiDataContractHandler(registry) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index cb65dff8a3..a8e3aac6f0 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -1,9 +1,12 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata import android.content.Intent +import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.google.gson.reflect.TypeToken import io.reactivex.Flowable import io.reactivex.Observable @@ -12,6 +15,8 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.BehaviorProcessor import org.dhis2.R import org.dhis2.bindings.profilePicturePath +import org.dhis2.commons.Constants +import org.dhis2.commons.bindings.canCreateEventInEnrollment import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.bindings.event import org.dhis2.commons.bindings.program @@ -22,7 +27,6 @@ import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.forms.dataentry.RuleEngineRepository import org.dhis2.form.data.FormValueStore @@ -32,8 +36,10 @@ import org.dhis2.usescases.events.ScheduledEventActivity.Companion.getIntent import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity.Companion.getActivityBundle import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity +import org.dhis2.usescases.programStageSelection.ProgramStageSelectionActivity import org.dhis2.usescases.teiDashboard.DashboardProgramModel import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.decrement import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.increment import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions @@ -42,7 +48,9 @@ import org.dhis2.utils.EventMode import org.dhis2.utils.Result import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.analytics.CREATE_EVENT_TEI import org.dhis2.utils.analytics.FOLLOW_UP +import org.dhis2.utils.analytics.TYPE_EVENT_TEI import org.dhis2.utils.dialFloatingActionButton.DialItem import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -68,10 +76,11 @@ class TEIDataPresenter( private val filterManager: FilterManager, private val filterRepository: FilterRepository, private val valueStore: FormValueStore, - private val resources: ResourceManager, private val optionsRepository: OptionsRepository, private val getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions, private val eventCreationOptionsMapper: EventCreationOptionsMapper, + private val contractHandler: TeiDataContractHandler, + private val dashboardActivityPresenter: TeiDashboardContracts.Presenter, ) { private val groupingProcessor: BehaviorProcessor = BehaviorProcessor.create() private val compositeDisposable: CompositeDisposable = CompositeDisposable() @@ -79,6 +88,10 @@ class TEIDataPresenter( private var currentStage: String = "" private var stagesToHide: List = emptyList() + private val _groupEvents = MutableLiveData(false) + private val _shouldDisplayEventCreationButton = MutableLiveData(false) + val shouldDisplayEventCreationButton: LiveData = _shouldDisplayEventCreationButton + fun init() { compositeDisposable.add( filterManager.asFlowable().startWith(filterManager) @@ -127,9 +140,10 @@ class TEIDataPresenter( currentStage = if (stageUid == currentStage && !showOptions) "" else stageUid StageSection(currentStage, showOptions) } - val groupingFlowable = groupingProcessor.startWith( - hasGrouping(it), - ) + val programHasGrouping = hasGrouping(it) + val groupingFlowable = groupingProcessor.startWith(programHasGrouping) + _groupEvents.value = programHasGrouping + compositeDisposable.add( Flowable.combineLatest( filterManager.asFlowable().startWith(filterManager), @@ -158,7 +172,7 @@ class TEIDataPresenter( .observeOn(schedulerProvider.ui()) .subscribe( { events -> - view.setEvents(events, canAddNewEvents()) + view.setEvents(events) decrement() }, Timber.Forest::d, @@ -188,7 +202,12 @@ class TEIDataPresenter( Timber.Forest::e, ), ) - } ?: view.setEnrollmentData(null, null) + } ?: run { + view.setEnrollmentData(null, null) + _shouldDisplayEventCreationButton.value = false + } + + updateCreateEventButtonVisibility(_groupEvents.value ?: false) compositeDisposable.add( Single.zip( @@ -229,6 +248,16 @@ class TEIDataPresenter( ) } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun updateCreateEventButtonVisibility(isGrouping: Boolean) { + val enrollment = d2.enrollment(enrollmentUid) + val showButton = + enrollment != null && + !isGrouping && enrollment.status() == EnrollmentStatus.ACTIVE && + canAddNewEvents() + _shouldDisplayEventCreationButton.value = showButton + } + private fun hasGrouping(programUid: String): Boolean { var hasGrouping = true if (grouping.containsKey(programUid)) { @@ -244,9 +273,7 @@ class TEIDataPresenter( Timber.d("APPLYING EFFECTS") if (calcResult.error() != null) { Timber.e(calcResult.error()) - view.showProgramRuleErrorMessage( - resources.getString(R.string.error_applying_rule_effects), - ) + view.showProgramRuleErrorMessage() return emptyList() } val (_, _, _, _, _, _, stagesToHide1) = RulesUtilsProviderImpl( @@ -353,6 +380,40 @@ class TEIDataPresenter( view.switchFollowUp(followup) } + fun onEventCreationClick(eventCreationId: Int) { + createEventInEnrollment(eventCreationOptionsMapper.getActionType(eventCreationId)) + } + + fun onAcceptScheduleNewEvent(stageStandardInterval: Int) { + createEventInEnrollment(EventCreationType.SCHEDULE, stageStandardInterval) + } + + private fun createEventInEnrollment( + eventCreationType: EventCreationType, + scheduleIntervalDays: Int = 0, + ) { + analyticsHelper.setEvent(TYPE_EVENT_TEI, eventCreationType.name, CREATE_EVENT_TEI) + val bundle = Bundle() + bundle.putString( + Constants.PROGRAM_UID, + dashboardModel?.currentEnrollment?.program(), + ) + bundle.putString(Constants.TRACKED_ENTITY_INSTANCE, dashboardModel?.tei?.uid()) + dashboardModel?.currentOrgUnit?.uid() + ?.takeIf { enrollmentOrgUnitInCaptureScope(it) }?.let { + bundle.putString(Constants.ORG_UNIT, it) + } + + bundle.putString(Constants.ENROLLMENT_UID, dashboardModel?.currentEnrollment?.uid()) + bundle.putString(Constants.EVENT_CREATION_TYPE, eventCreationType.name) + bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, scheduleIntervalDays) + val intent = Intent(view.context, ProgramStageSelectionActivity::class.java) + intent.putExtras(bundle) + contractHandler.createEvent(intent).observe(view.viewLifecycleOwner()) { + dashboardActivityPresenter.init() + } + } + fun onScheduleSelected(uid: String?, sharedView: View?) { uid?.let { val intent = getIntent(view.context, uid) @@ -431,7 +492,9 @@ class TEIDataPresenter( Preference.GROUPING, groups, ) + _groupEvents.value = shouldGroupBool groupingProcessor.onNext(shouldGroupBool) + updateCreateEventButtonVisibility(shouldGroupBool) } } @@ -481,12 +544,7 @@ class TEIDataPresenter( } private fun canAddNewEvents(): Boolean { - return d2.enrollmentModule() - .enrollmentService() - .blockingGetAllowEventCreation( - enrollmentUid, - stagesToHide, - ) + return d2.canCreateEventInEnrollment(enrollmentUid, stagesToHide) } fun getOrgUnitName(orgUnitUid: String): String { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt new file mode 100644 index 0000000000..8577a0dd37 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt @@ -0,0 +1,56 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata + +import android.content.Intent +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import javax.inject.Inject + +class TeiDataContractHandler @Inject constructor( + registry: ActivityResultRegistry, +) { + + private val contractNewEventResult = MutableLiveData() + private val contractEditEvent = MutableLiveData() + private val contractScheduleEvent = MutableLiveData() + + private val createEvent = + registry.register(REGISTRY_NEW_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractNewEventResult.value = Unit + } + private val editEvent = + registry.register(REGISTRY_EDIT_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractEditEvent.value = Unit + } + + private val scheduleEvent = + registry.register(REGISTRY_SCHEDULE_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractScheduleEvent.value = Unit + } + + fun createEvent(intent: Intent): LiveData { + createEvent.launch(intent) + return contractNewEventResult + } + + fun editEvent(intent: Intent): LiveData { + editEvent.launch(intent) + return contractEditEvent + } + + fun scheduleEvent( + intent: Intent, + options: ActivityOptionsCompat = ActivityOptionsCompat.makeBasic(), + ): LiveData { + scheduleEvent.launch(intent, options) + return contractScheduleEvent + } + + companion object { + private const val REGISTRY_NEW_EVENT = "New Event" + private const val REGISTRY_EDIT_EVENT = "Edit Event" + private const val REGISTRY_SCHEDULE_EVENT = "Schedule Event" + } +} diff --git a/app/src/main/res/layout-land/fragment_tei_data.xml b/app/src/main/res/layout-land/fragment_tei_data.xml index ba827db9ad..6803452894 100644 --- a/app/src/main/res/layout-land/fragment_tei_data.xml +++ b/app/src/main/res/layout-land/fragment_tei_data.xml @@ -33,14 +33,6 @@ name="followup" type="androidx.databinding.ObservableBoolean" /> - - - - diff --git a/app/src/main/res/layout/fragment_tei_data.xml b/app/src/main/res/layout/fragment_tei_data.xml index bd77458dab..e30b7e12ed 100644 --- a/app/src/main/res/layout/fragment_tei_data.xml +++ b/app/src/main/res/layout/fragment_tei_data.xml @@ -32,15 +32,6 @@ - - - - - + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintVertical_weight="0" + tools:listitem="@layout/item_event" /> - + diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt index e058268330..be5250af38 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt @@ -38,6 +38,7 @@ class SearchTEIViewModelTest { private val initialProgram = "programUid" private val initialQuery = mutableMapOf() private val repository: SearchRepository = mock() + private val repositoryKt: SearchRepositoryKt = mock() private val pageConfigurator: SearchPageConfigurator = mock() private val mapDataRepository: MapDataRepository = mock() private val networkUtils: NetworkUtils = mock() @@ -59,6 +60,7 @@ class SearchTEIViewModelTest { initialProgram, initialQuery, repository, + repositoryKt, pageConfigurator, mapDataRepository, networkUtils, @@ -187,7 +189,7 @@ class SearchTEIViewModelTest { setCurrentProgram(testingProgram) viewModel.fetchListResults {} testingDispatcher.scheduler.advanceUntilIdle() - verify(repository).searchTrackedEntities( + verify(repositoryKt).searchTrackedEntities( SearchParametersModel( selectedProgram = testingProgram, queryData = mutableMapOf(), @@ -202,7 +204,7 @@ class SearchTEIViewModelTest { setCurrentProgram(testingProgram) viewModel.fetchListResults {} - verify(repository, times(0)).searchTrackedEntities( + verify(repositoryKt, times(0)).searchTrackedEntities( SearchParametersModel( selectedProgram = testingProgram, queryData = mutableMapOf(), @@ -210,7 +212,7 @@ class SearchTEIViewModelTest { true, ) - verify(repository, times(0)).searchTrackedEntities( + verify(repositoryKt, times(0)).searchTrackedEntities( SearchParametersModel( selectedProgram = testingProgram, queryData = mutableMapOf(), diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt index a0e3d67478..968de083d0 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt @@ -1,6 +1,13 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.data +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.MutableLiveData import io.reactivex.Single +import org.dhis2.commons.bindings.canCreateEventInEnrollment +import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.data.EventViewModel import org.dhis2.commons.data.EventViewModelType import org.dhis2.commons.filters.FilterManager @@ -12,17 +19,22 @@ import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardPresenter import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.EventCreationOptionsMapper import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataContracts import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataPresenter +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataContractHandler import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataRepository import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions import org.dhis2.utils.analytics.AnalyticsHelper import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mockito import org.mockito.kotlin.any @@ -34,6 +46,10 @@ import java.util.Date class TeiDataPresenterTest { + @Rule + @JvmField + var instantExecutorRule = InstantTaskExecutorRule() + private val view: TEIDataContracts.View = mock() private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) private val dashboardRepository: DashboardRepository = mock() @@ -49,10 +65,12 @@ class TeiDataPresenterTest { private val filterRepository: FilterRepository = mock() private lateinit var teiDataPresenter: TEIDataPresenter private val valueStore: FormValueStore = mock() - private val resources: ResourceManager = mock() private val optionsRepository: OptionsRepository = mock() private val getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions = mock() - private val eventCreationOptionsMapper: EventCreationOptionsMapper = mock() + private val resources: ResourceManager = mock() + private val eventCreationOptionsMapper = EventCreationOptionsMapper(resources) + private val teiDataContractHandler: TeiDataContractHandler = mock() + private val activityPresenter: TeiDashboardPresenter = mock() @Before fun setUp() { @@ -71,10 +89,11 @@ class TeiDataPresenterTest { filterManager, filterRepository, valueStore, - resources, optionsRepository, getNewEventCreationTypeOptions, eventCreationOptionsMapper, + teiDataContractHandler, + activityPresenter, ) } @@ -143,7 +162,47 @@ class TeiDataPresenterTest { assert(stage.applyHideStage(true) == stage) } - private fun fakeModel(eventCount: Int = 0, type: EventViewModelType = EventViewModelType.STAGE): EventViewModel { + @Test + fun shouldSuccessfullyCreateANewEvent() { + val lifecycleOwner: LifecycleOwner = Mockito.mock(LifecycleOwner::class.java) + val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java)) + lifecycle.currentState = Lifecycle.State.RESUMED + Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle) + + val contractLiveData = MutableLiveData() + whenever(view.viewLifecycleOwner()) doReturn lifecycleOwner + whenever(teiDataContractHandler.createEvent(any())) doReturn contractLiveData + teiDataPresenter.onEventCreationClick(EventCreationOptionsMapper.ADD_NEW_ID) + contractLiveData.value = Unit + verify(activityPresenter).init() + } + + @Test + fun shouldNotBeAbleToCreateNewEventsWhenFull() { + val mockedEnrollment = mock { + on { status() } doReturn EnrollmentStatus.ACTIVE + } + whenever(d2.enrollment(enrollmentUid)) doReturn mockedEnrollment + whenever(d2.canCreateEventInEnrollment(enrollmentUid, emptyList())) doReturn false + teiDataPresenter.updateCreateEventButtonVisibility(false) + assertTrue(teiDataPresenter.shouldDisplayEventCreationButton.value == false) + } + + @Test + fun shouldNotBeAbleToCreateNewEventsWhenEnrollmentNotActive() { + val mockedEnrollment = mock { + on { status() } doReturn EnrollmentStatus.CANCELLED + } + whenever(d2.enrollment(enrollmentUid)) doReturn mockedEnrollment + whenever(d2.canCreateEventInEnrollment(enrollmentUid, emptyList())) doReturn true + teiDataPresenter.updateCreateEventButtonVisibility(false) + assertTrue(teiDataPresenter.shouldDisplayEventCreationButton.value == false) + } + + private fun fakeModel( + eventCount: Int = 0, + type: EventViewModelType = EventViewModelType.STAGE, + ): EventViewModel { val dataElements = mutableListOf>() dataElements.add( Pair("Name", "Peter"), diff --git a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt index 1c5ff1a888..a000609fd8 100644 --- a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt +++ b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt @@ -4,10 +4,13 @@ import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.work.WorkInfo import io.reactivex.Completable import io.reactivex.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.dhis2.commons.Constants import org.dhis2.commons.sync.ConflictType import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.viewmodel.DispatcherProvider @@ -15,11 +18,16 @@ import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.data.service.workManager.WorkManagerController import org.dhis2.usescases.sms.SmsSendingService import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.category.CategoryOptionCombo +import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSetElement import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyList import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -283,4 +291,176 @@ class GranularSyncPresenterTest { verify(repository).getUiState() } + + @Test + fun shouldSyncProgram() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.TrackerProgram("programUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncTei() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.TrackerProgramTei("enrollmentUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncEvent() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Event("eventUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncDataSet() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.DataSet("dataSetUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncDataValues() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.DataSetInstance( + "dataSetUid", + "periodId", + "orgUnitUid", + "attrOptionComboUid", + ), + workManager, + smsSyncProvider, + ) + + val mockedDataSet: DataSet = mock { + on { dataSetElements() } doReturn listOf( + DataSetElement.builder() + .categoryCombo(ObjectWithUid.create("catComboUid")) + .dataElement(ObjectWithUid.create("dataElementUid")) + .dataSet(ObjectWithUid.create("dataSetUid")) + .build(), + ) + } + whenever( + d2.dataSetModule().dataSets().withDataSetElements().uid(any()).get(), + ) doReturn Single.just(mockedDataSet) + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid(), + ) doReturn mock() + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid().`in`(anyList()), + ) doReturn mock() + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid().`in`(anyList()).get(), + ) doReturn Single.just( + listOf(CategoryOptionCombo.builder().uid("catComboUid").build()), + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldPerformInitialSync() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Global(), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).syncDataForWorker(Constants.DATA_NOW, Constants.INITIAL_SYNC) + verify(workInfoObserver).onChanged(anyList()) + } } diff --git a/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt index 798de30b5b..6a34aef226 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt @@ -116,6 +116,13 @@ fun D2.enrollmentImportConflicts(enrollmentUid: String): List): Boolean = + enrollmentModule().enrollmentService() + .blockingGetAllowEventCreation( + enrollmentUid, + stagesToHide, + ) + fun D2.teiAttribute(attributeUid: String) = trackedEntityModule().trackedEntityAttributes() .uid(attributeUid).blockingGet() diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt index e1a4e08a17..43e8955d4c 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt @@ -41,6 +41,7 @@ import org.dhis2.composetable.ui.DataSetTableScreen import org.dhis2.composetable.ui.DataTable import org.dhis2.composetable.ui.DrawableId import org.dhis2.composetable.ui.INPUT_ERROR_MESSAGE_TEST_TAG +import org.dhis2.composetable.ui.INPUT_HELPER_TEXT_TEST_TAG import org.dhis2.composetable.ui.INPUT_ICON_TEST_TAG import org.dhis2.composetable.ui.INPUT_TEST_FIELD_TEST_TAG import org.dhis2.composetable.ui.INPUT_TEST_TAG @@ -126,6 +127,7 @@ class TableRobot( fakeModelType: FakeModelType, tableAppScreenOptions: TableAppScreenOptions = TableAppScreenOptions(), tableConfiguration: TableConfiguration = TableConfiguration(headerActionsEnabled = true), + helperText: String? = null, onSave: (TableCell) -> Unit = {} ): List { var fakeModel: List = emptyList() @@ -152,6 +154,7 @@ class TableRobot( secondaryLabels = fakeModel.find { it.id == tableId }?.tableHeaderModel?.rows?.map { it.cells[cell.column!! % it.cells.size].value } ?: emptyList(), + helperText = helperText, currentValue = cell.value, keyboardInputType = KeyboardInputType.TextInput(), error = null @@ -345,6 +348,12 @@ class TableRobot( .assertTextEquals(expectedErrorMessage) } + fun assertInputComponentHelperTextIsDisplayed(expectedHelperText: String) { + composeTestRule.onNodeWithTag(INPUT_HELPER_TEXT_TEST_TAG) + .assertIsDisplayed() + .assertTextEquals(expectedHelperText) + } + fun assertCellWithErrorSetsErrorMessage( rowIndex: Int, columnIndex: Int, diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/data/TableInputUiData.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/data/TableInputUiData.kt index 4919beb2bd..26f8e71d3f 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/data/TableInputUiData.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/data/TableInputUiData.kt @@ -39,7 +39,8 @@ val tableData = listOf( ), values = mapOf( Pair(0, TableCell("00", 0, 0, "12")), - Pair(1, TableCell("01", 0, 1, value = "-1", error = input_error_message)) + Pair(1, TableCell("01", 0, 1, value = "-1", error = input_error_message)), + Pair(2, TableCell("02", 0, 2, "text")), ), ), TableRowModel( diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt index 66934bfb92..5cb163c10b 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/ui/TextInputUiTest.kt @@ -77,9 +77,26 @@ class TextInputUiTest { } } + @Test + fun shouldDisplayHelperText() { + val helperText = "This is a helper Text" + + tableRobot(composeTestRule) { + val fakeModels = initTableAppScreen( + FakeModelType.MANDATORY_TABLE, + helperText = helperText + ) + clickOnCell(fakeModels.first().id!!, 0, 0) + assertInputComponentHelperTextIsDisplayed(helperText) + } + } + @OptIn(ExperimentalMaterialApi::class) @Composable - private fun TextInputUiTestScreen(onSave: (TableCell) -> Unit) { + private fun TextInputUiTestScreen( + helperText: String? = null, + onSave: (TableCell) -> Unit + ) { val bottomSheetState = rememberBottomSheetScaffoldState( bottomSheetState = rememberBottomSheetState(initialValue = BottomSheetValue.Collapsed) ) @@ -90,7 +107,9 @@ class TextInputUiTest { } var currentInputType by remember { mutableStateOf( - TextInputModel() + TextInputModel( + helperText = helperText + ) ) } diff --git a/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt b/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt index 28d51cc9b6..fc3d10d08f 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt @@ -6,6 +6,7 @@ data class TextInputModel( val id: String = "", val mainLabel: String = "", val secondaryLabels: List = emptyList(), + val helperText: String? = null, val currentValue: String? = null, val keyboardInputType: KeyboardInputType = KeyboardInputType.TextInput(), val selection: TextRange? = null, @@ -18,4 +19,6 @@ data class TextInputModel( fun hasErrorOrWarning() = errorOrWarningMessage() != null fun actionIconCanBeClicked(hasFocus: Boolean) = hasFocus && error == null + + fun hasHelperText() = helperText?.isNotEmpty() ?: false } diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt index 545daa3c6f..61c1b6f3d1 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt @@ -4,6 +4,7 @@ import android.graphics.Rect import android.view.ViewTreeObserver import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -72,6 +73,7 @@ fun TextInput( shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), ) .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 4.dp), + verticalArrangement = spacedBy(8.dp), ) { InputTitle(textInputModel.mainLabel, textInputModel.secondaryLabels) TextInputContent( @@ -252,6 +254,17 @@ private fun TextInputContent( ), ) } + if (textInputModel.hasHelperText()) { + Text( + modifier = Modifier + .testTag(INPUT_HELPER_TEXT_TEST_TAG), + text = textInputModel.helperText!!, + style = TextStyle( + color = LocalTableColors.current.headerText, + ), + fontSize = 10.sp, + ) + } } } @@ -335,6 +348,26 @@ fun DefaultTextInputStatusPreview() { id = "", mainLabel = "Row", secondaryLabels = listOf("header 1", "header 2"), + helperText = "description", + currentValue = "Test", + ) + + TextInput( + textInputModel = previewTextInput, + textInputInteractions = object : TextInputInteractions {}, + focusRequester = FocusRequester(), + ) +} + +@Preview +@Composable +fun DefaultTextInputErrorStatusPreview() { + val previewTextInput = TextInputModel( + id = "", + mainLabel = "Row", + secondaryLabels = listOf("header 1", "header 2"), + error = "error message", + helperText = "description", currentValue = "Test", ) @@ -349,6 +382,7 @@ const val INPUT_TEST_TAG = "INPUT_TEST_TAG" const val INPUT_TEST_FIELD_TEST_TAG = "INPUT_TEST_FIELD_TEST_TAG" const val INPUT_ERROR_MESSAGE_TEST_TAG = "INPUT_ERROR_MESSAGE_TEST_TAG" const val INPUT_ICON_TEST_TAG = "INPUT_ICON_TEST_TAG" +const val INPUT_HELPER_TEXT_TEST_TAG = "INPUT_HELPER_TEXT_TEST_TAG" val DrawableId = SemanticsPropertyKey("DrawableResId") var SemanticsPropertyReceiver.drawableId by DrawableId diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt index 894d753402..6f7b795d21 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt @@ -1,19 +1,21 @@ package org.dhis2.form.data -import org.dhis2.commons.bindings.disableCollapsableSectionsInProgram +import org.dhis2.form.data.metadata.FormBaseConfiguration import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.SectionUiModelImpl import org.dhis2.form.ui.FieldViewModelFactory -import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.imports.TrackerImportConflict import org.hisp.dhis.android.core.program.SectionRenderingType abstract class DataEntryBaseRepository( - private val d2: D2, + private val conf: FormBaseConfiguration, private val fieldFactory: FieldViewModelFactory, ) : DataEntryRepository { abstract val programUid: String? + override fun firstSectionToOpen(): String? { + return sectionUids().blockingFirst().firstOrNull() + } override fun updateSection( sectionToUpdate: FieldUiModel, @@ -64,10 +66,7 @@ abstract class DataEntryBaseRepository( private fun optionsFromGroups(optionGroupUids: List): List { if (optionGroupUids.isEmpty()) return emptyList() val optionsFromGroups = arrayListOf() - val optionGroups = d2.optionModule().optionGroups() - .withOptions() - .byUid().`in`(optionGroupUids) - .blockingGet() + val optionGroups = conf.optionGroups(optionGroupUids) for (optionGroup in optionGroups) { for (option in optionGroup.options()!!) { if (!optionsFromGroups.contains(option.uid())) { @@ -106,6 +105,6 @@ abstract class DataEntryBaseRepository( } override fun disableCollapsableSections(): Boolean? { - return programUid?.let { d2.disableCollapsableSectionsInProgram(programUid = it) } + return programUid?.let { conf.disableCollapsableSectionsInProgram(programUid = it) } } } diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt index 9da98dc173..41a3ee0213 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt @@ -5,6 +5,7 @@ import org.dhis2.form.model.FieldUiModel interface DataEntryRepository { fun list(): Flowable> + fun firstSectionToOpen(): String? fun sectionUids(): Flowable> fun updateSection( sectionToUpdate: FieldUiModel, diff --git a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt index 03e3dd0b91..5dc3066aa0 100644 --- a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt @@ -2,97 +2,78 @@ package org.dhis2.form.data import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.bindings.userFriendlyValue import org.dhis2.commons.date.DateUtils import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.model.EnrollmentMode import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.model.SectionUiModelImpl.Companion.SINGLE_SECTION_UID import org.dhis2.form.ui.FieldViewModelFactory import org.dhis2.form.ui.provider.EnrollmentFormLabelsProvider -import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList -import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.ObjectStyle import org.hisp.dhis.android.core.common.ValueType -import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository import org.hisp.dhis.android.core.imports.ImportStatus -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit -import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramSection import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute import org.hisp.dhis.android.core.program.SectionRenderingType import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueObjectRepository import timber.log.Timber class EnrollmentRepository( private val fieldFactory: FieldViewModelFactory, - private val enrollmentUid: String, - private val d2: D2, + private val conf: EnrollmentConfiguration, private val enrollmentMode: EnrollmentMode, private val enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider, -) : DataEntryBaseRepository(d2, fieldFactory) { +) : DataEntryBaseRepository(conf, fieldFactory) { - private val enrollmentRepository: EnrollmentObjectRepository = - d2.enrollmentModule().enrollments().uid(enrollmentUid) - - private val program by lazy { - d2.programModule().programs().uid(enrollmentRepository.blockingGet()?.program()).get() + override val programUid by lazy { + conf.program()?.uid() } - override val programUid by lazy { - program.blockingGet()?.uid() + private val programSections by lazy { + conf.sections() } private fun canBeEdited(): Boolean { - val selectedProgram = d2.programModule().programs().uid( - d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()?.program(), - ).blockingGet() + val selectedProgram = conf.program() val programAccess = selectedProgram?.access()?.data()?.write() == true - val teTypeAccess = d2.trackedEntityModule().trackedEntityTypes().uid( - selectedProgram?.trackedEntityType()?.uid(), - ).blockingGet()?.access()?.data()?.write() == true + val teTypeAccess = conf.trackedEntityType()?.access()?.data()?.write() == true return programAccess && teTypeAccess } - private val programSections by lazy { - d2.programModule().programSections().withAttributes() - .byProgramUid().eq(enrollmentRepository.blockingGet()?.program()) - .blockingGet() - } - override fun sectionUids(): Flowable> { val sectionUids = mutableListOf(ENROLLMENT_DATA_SECTION_UID) - sectionUids.addAll(programSections.map { it.uid() }) + if (programSections.isEmpty()) { + sectionUids.add(SINGLE_SECTION_UID) + } else { + sectionUids.addAll(programSections.map { it.uid() }) + } return Flowable.just(sectionUids) } override fun list(): Flowable> { - return program - .flatMap { program -> - d2.programModule().programSections().byProgramUid().eq(program.uid()) - .withAttributes().get() - .flatMap { programSections -> - if (programSections.isEmpty()) { - getFieldsForSingleSection() - .map { singleSectionList -> - val list = getSingleSectionList() - list.addAll(singleSectionList) - list - } - } else { - getFieldsForMultipleSections() + return Single.just(conf.sections()) + .flatMap { programSections -> + if (programSections.isEmpty()) { + getFieldsForSingleSection() + .map { singleSectionList -> + val list = getSingleSectionList() + list.addAll(singleSectionList) + list } - }.map { list -> - val fields = getEnrollmentData(program) - fields.addAll(list) - fields.add(fieldFactory.createClosingSection()) - fields.toList() - } - }.toFlowable() + } else { + getFieldsForMultipleSections() + } + }.map { list -> + val fields = getEnrollmentData() + fields.addAll(list) + fields.add(fieldFactory.createClosingSection()) + fields.toList() + } + .toFlowable() } override fun isEvent(): Boolean { @@ -100,11 +81,7 @@ class EnrollmentRepository( } private fun getSingleSectionList(): MutableList { - val tei = d2.trackedEntityModule().trackedEntityInstances() - .uid(enrollmentRepository.blockingGet()?.trackedEntityInstance()) - .blockingGet() - val teiType = d2.trackedEntityModule().trackedEntityTypes() - .uid(tei?.trackedEntityType()).blockingGet() + val teiType = conf.trackedEntityType() return mutableListOf( fieldFactory.createSingleSection( String.format( @@ -117,13 +94,7 @@ class EnrollmentRepository( private fun getFieldsForSingleSection(): Single> { return Single.fromCallable { - val programAttributes = - d2.programModule().programTrackedEntityAttributes().withRenderType() - .byProgram().eq(program.blockingGet()?.uid()) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - - programAttributes.map { programTrackedEntityAttribute -> + conf.programAttributes().map { programTrackedEntityAttribute -> transform(programTrackedEntityAttribute) } } @@ -137,12 +108,9 @@ class EnrollmentRepository( transformSection(section.uid(), section.displayName(), section.description()), ) section.attributes()?.forEachIndexed { _, attribute -> - d2.programModule().programTrackedEntityAttributes().withRenderType() - .byProgram().eq(program.blockingGet()?.uid()) - .byTrackedEntityAttribute().eq(attribute.uid()) - .one().blockingGet()?.let { programTrackedEntityAttribute -> - fields.add(transform(programTrackedEntityAttribute, section.uid())) - } + conf.programAttribute(attribute.uid())?.let { programTrackedEntityAttribute -> + fields.add(transform(programTrackedEntityAttribute, section.uid())) + } } } return@fromCallable fields @@ -153,33 +121,28 @@ class EnrollmentRepository( programTrackedEntityAttribute: ProgramTrackedEntityAttribute, sectionUid: String? = SINGLE_SECTION_UID, ): FieldUiModel { - val attribute = d2.trackedEntityModule().trackedEntityAttributes() - .uid(programTrackedEntityAttribute.trackedEntityAttribute()!!.uid()) - .blockingGet() - val attrValueRepository = d2.trackedEntityModule().trackedEntityAttributeValues() - .value( - attribute!!.uid(), - enrollmentRepository.blockingGet()!!.trackedEntityInstance()!!, - ) + val attribute = programTrackedEntityAttribute.trackedEntityAttribute()?.uid()?.let { + conf.trackedEntityAttribute(it) + } ?: throw IllegalStateException( + "Attribute %s does not exist".format( + programTrackedEntityAttribute.trackedEntityAttribute()?.uid(), + ), + ) val valueType = attribute.valueType() - var mandatory = programTrackedEntityAttribute.mandatory()!! + var mandatory = programTrackedEntityAttribute.mandatory() ?: false val optionSet = attribute.optionSet()?.uid() - val generated = attribute.generated()!! + val generated = attribute.generated() ?: false - val orgUnitUid = enrollmentRepository.blockingGet()!!.organisationUnit() + val orgUnitUid = conf.enrollment() + ?.organisationUnit() - var dataValue: String? = getAttributeValue(attrValueRepository) + var dataValue: String? = attribute.uid() + ?.let { conf.attributeValue(it) } var optionSetConfig: OptionSetConfiguration? = null if (!optionSet.isNullOrEmpty()) { - val optionCount = - d2.optionModule().options().byOptionSetUid().eq(optionSet).blockingCount() - optionSetConfig = OptionSetConfiguration.config(optionCount) { - d2.optionModule().options().byOptionSetUid().eq(optionSet) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) - .blockingGet() - } + optionSetConfig = conf.optionSetConfig(optionSet) } var (error, warning) = getConflictErrorsAndWarnings(attribute.uid(), dataValue) @@ -190,14 +153,14 @@ class EnrollmentRepository( dataValue = result.first warning = result.second if (!dataValue.isNullOrEmpty()) { - attrValueRepository.blockingSet(dataValue) + conf.setValue(attribute.uid(), dataValue) } } if ((valueType == ValueType.ORGANISATION_UNIT || valueType?.isDate == true) && !dataValue.isNullOrEmpty() ) { - dataValue = attrValueRepository.blockingGet()?.value() + dataValue = conf.getValue(attribute.uid())?.value() } var programSection: ProgramSection? = null @@ -247,9 +210,7 @@ class EnrollmentRepository( var error: String? = null var warning: String? = null - val conflicts = d2.importModule().trackerImportConflicts() - .byEnrollmentUid().eq(enrollmentUid) - .blockingGet() + val conflicts = conf.conflicts() val conflict = conflicts .find { it.trackedEntityAttribute() == attributeUid } @@ -268,14 +229,6 @@ class EnrollmentRepository( private fun getSectionRenderingType(programSection: ProgramSection?) = programSection?.renderType()?.mobile()?.type() - private fun getAttributeValue( - attrValueRepository: TrackedEntityAttributeValueObjectRepository, - ) = if (attrValueRepository.blockingExists()) { - attrValueRepository.blockingGet()?.userFriendlyValue(d2) - } else { - null - } - private fun handleAutogeneratedValue( attr: TrackedEntityAttribute, orgUnitUid: String, @@ -283,12 +236,11 @@ class EnrollmentRepository( var warning: String? = null var dataValue: String? = null try { - val teiUid = enrollmentRepository.blockingGet()!!.trackedEntityInstance() + val teiUid = conf.tei() if (teiUid != null) { try { - dataValue = d2.trackedEntityModule().reservedValueManager() - .blockingGetValue(attr.uid(), orgUnitUid) + dataValue = conf.fetchAutogeneratedValue(attr.uid(), orgUnitUid) } catch (e: Exception) { dataValue = null warning = enrollmentFormLabelsProvider.provideReservedValueWarning() @@ -296,8 +248,7 @@ class EnrollmentRepository( if (attr.valueType() == ValueType.NUMBER) { while (dataValue!!.startsWith("0")) { - dataValue = d2.trackedEntityModule().reservedValueManager() - .blockingGetValue(attr.uid(), orgUnitUid) + dataValue = conf.fetchAutogeneratedValue(attr.uid(), orgUnitUid) } } } @@ -309,47 +260,43 @@ class EnrollmentRepository( return Pair(dataValue, warning) } - private fun getEnrollmentData(program: Program): MutableList { + private fun getEnrollmentData(): MutableList { val enrollmentDataList = ArrayList() - enrollmentDataList.add(getEnrollmentDataSection(program.description())) + enrollmentDataList.add(getEnrollmentDataSection(conf.program()?.description())) enrollmentDataList.add( getEnrollmentDateField( - program.enrollmentDateLabel() + conf.program()?.enrollmentDateLabel() ?: enrollmentFormLabelsProvider.provideEnrollmentDateDefaultLabel(), - program.selectEnrollmentDatesInFuture(), + conf.program()?.selectEnrollmentDatesInFuture(), ), ) - if (program.displayIncidentDate()!!) { + if (conf.program()?.displayIncidentDate()!!) { enrollmentDataList.add( getIncidentDateField( - program.incidentDateLabel() + conf.program()?.incidentDateLabel() ?: enrollmentFormLabelsProvider.provideIncidentDateDefaultLabel(), - program.selectIncidentDatesInFuture(), + conf.program()?.selectIncidentDatesInFuture(), ), ) } - val programUids = - enrollmentRepository.blockingGet()?.program()?.let { listOf(it) } ?: emptyList() - val orgUnits = d2.organisationUnitModule().organisationUnits() - .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) - .byProgramUids(programUids).blockingCount() + + val orgUnits = conf.captureOrgUnitsCount() enrollmentDataList.add( getOrgUnitField(enrollmentMode == EnrollmentMode.NEW && orgUnits > 1), ) - val teiType = - d2.trackedEntityModule().trackedEntityTypes() - .uid(program.trackedEntityType()!!.uid()) - .blockingGet() + val teiType = conf.trackedEntityType() if (teiType!!.featureType() != null && teiType.featureType() != FeatureType.NONE) { enrollmentDataList.add(getTeiCoordinatesField(teiType.featureType())) } - if (program.featureType() != null && program.featureType() != FeatureType.NONE) { + if (conf.program()?.featureType() != null && conf.program() + ?.featureType() != FeatureType.NONE + ) { enrollmentDataList.add( getEnrollmentCoordinatesField( - program.featureType(), + conf.program()?.featureType(), ), ) } @@ -379,7 +326,7 @@ class EnrollmentRepository( ValueType.DATE, true, // check in constructor of dateviewmodel null, - when (val date = enrollmentRepository.blockingGet()!!.enrollmentDate()) { + when (val date = conf.enrollment()?.enrollmentDate()) { null -> null else -> DateUtils.oldUiDateFormat().format(date) }, @@ -406,7 +353,7 @@ class EnrollmentRepository( ValueType.DATE, true, null, - when (val date = enrollmentRepository.blockingGet()!!.incidentDate()) { + when (val date = conf.enrollment()?.incidentDate()) { null -> null else -> DateUtils.oldUiDateFormat().format(date) }, @@ -430,7 +377,7 @@ class EnrollmentRepository( ValueType.ORGANISATION_UNIT, true, null, - enrollmentRepository.blockingGet()?.organisationUnit(), + conf.enrollment()?.organisationUnit(), ENROLLMENT_DATA_SECTION_UID, null, editable, @@ -446,12 +393,8 @@ class EnrollmentRepository( } private fun getTeiCoordinatesField(featureType: FeatureType?): FieldUiModel { - val tei = d2.trackedEntityModule().trackedEntityInstances() - .uid( - enrollmentRepository.blockingGet()!!.trackedEntityInstance(), - ).blockingGet() - val teiType = d2.trackedEntityModule().trackedEntityTypes() - .uid(tei?.trackedEntityType()).blockingGet() + val tei = conf.tei() + val teiType = conf.trackedEntityType() val teiCoordinatesLabel = enrollmentFormLabelsProvider.provideTeiCoordinatesLabel() return fieldFactory.create( TEI_COORDINATES_UID, @@ -459,7 +402,7 @@ class EnrollmentRepository( ValueType.COORDINATE, false, null, - if (tei!!.geometry() != null) tei.geometry()!!.coordinates() else null, + tei?.geometry()?.coordinates(), ENROLLMENT_DATA_SECTION_UID, null, canBeEdited(), @@ -480,11 +423,7 @@ class EnrollmentRepository( ValueType.COORDINATE, false, null, - if (enrollmentRepository.blockingGet()!!.geometry() != null) { - enrollmentRepository.blockingGet()!!.geometry()!!.coordinates() - } else { - null - }, + conf.enrollment()?.geometry()?.coordinates(), ENROLLMENT_DATA_SECTION_UID, null, canBeEdited(), @@ -499,41 +438,47 @@ class EnrollmentRepository( } fun hasEventsGeneratedByEnrollmentDate(): Boolean { - val enrollment = enrollmentRepository.blockingGet() ?: return false - - val stagesWithReportDateToUse = d2.programModule().programStages() - .byProgramUid().eq(enrollment.program()) - .byOpenAfterEnrollment().isTrue - .byReportDateToUse().eq("enrollmentDate") - .blockingGetUids() - val stagesWithGeneratedBy = d2.programModule().programStages() - .byProgramUid().eq(enrollment.program()) - .byAutoGenerateEvent().isTrue - .byGeneratedByEnrollmentDate().isTrue - .blockingGetUids() - return !d2.eventModule().events() - .byEnrollmentUid().eq(enrollmentUid) - .byProgramStageUid().`in`(stagesWithReportDateToUse.union(stagesWithGeneratedBy)) - .blockingIsEmpty() + return conf.hasEventsGeneratedByEnrollmentDate() } fun hasEventsGeneratedByIncidentDate(): Boolean { - val enrollment = enrollmentRepository.blockingGet() ?: return false - - val stagesWithReportDateToUse = d2.programModule().programStages() - .byProgramUid().eq(enrollment.program()) - .byOpenAfterEnrollment().isTrue - .byReportDateToUse().eq("incidentDate") - .blockingGetUids() - val stagesWithGeneratedBy = d2.programModule().programStages() - .byProgramUid().eq(enrollment.program()) - .byAutoGenerateEvent().isTrue - .byGeneratedByEnrollmentDate().isFalse - .blockingGetUids() - return !d2.eventModule().events() - .byEnrollmentUid().eq(enrollmentUid) - .byProgramStageUid().`in`(stagesWithReportDateToUse.union(stagesWithGeneratedBy)) - .blockingIsEmpty() + return conf.hasEventsGeneratedByIncidentDate() + } + + override fun firstSectionToOpen(): String? { + return if (enrollmentMode == EnrollmentMode.CHECK && isEnrollmentDataCompleted()) { + sectionUids().blockingFirst().filterIndexed { index, _ -> index != 0 }.firstOrNull() + } else { + super.firstSectionToOpen() + } + } + + private fun isEnrollmentDataCompleted(): Boolean { + val program = conf.program() + val enrollment = conf.enrollment() + + val hasEnrollmentDate = enrollment?.enrollmentDate() != null + if (!hasEnrollmentDate) return false + + if (program?.displayIncidentDate() == true) { + val hasIncidentDate = enrollment?.incidentDate() != null + if (!hasIncidentDate) return false + } + + val hasOrganisationUnit = enrollment?.organisationUnit() != null + if (!hasOrganisationUnit) return false + + if (conf.trackedEntityType()?.featureType() != FeatureType.NONE) { + val hasTeiCoordinates = conf.tei()?.geometry() != null + if (!hasTeiCoordinates) return false + } + + if (program?.featureType() != FeatureType.NONE) { + val hasEnrollmentCoordinates = enrollment?.geometry() != null + if (!hasEnrollmentCoordinates) return false + } + + return true } companion object { diff --git a/form/src/main/java/org/dhis2/form/data/EventRepository.kt b/form/src/main/java/org/dhis2/form/data/EventRepository.kt index d452cf7d0b..67fbb4b7bc 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -5,6 +5,7 @@ import io.reactivex.Flowable import io.reactivex.Single import org.dhis2.bindings.blockingGetValueCheck import org.dhis2.bindings.userFriendlyValue +import org.dhis2.form.data.metadata.FormBaseConfiguration import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.ui.FieldViewModelFactory @@ -22,7 +23,7 @@ class EventRepository( private val fieldFactory: FieldViewModelFactory, private val eventUid: String, private val d2: D2, -) : DataEntryBaseRepository(d2, fieldFactory) { +) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory) { private val event by lazy { d2.eventModule().events().uid(eventUid) diff --git a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt index 4d52a7041e..3f7ab17614 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt @@ -54,10 +54,10 @@ class FormRepositoryImpl( shouldOpenErrorLocation -> itemList.firstOrNull { it.error != null || it.warning != null }?.programStageSection - ?: dataEntryRepository?.sectionUids()?.blockingFirst()?.firstOrNull() + ?: dataEntryRepository?.firstSectionToOpen() else -> - dataEntryRepository?.sectionUids()?.blockingFirst()?.firstOrNull() + dataEntryRepository?.firstSectionToOpen() } override fun composeList(skipProgramRules: Boolean): List { diff --git a/form/src/main/java/org/dhis2/form/data/SearchRepository.kt b/form/src/main/java/org/dhis2/form/data/SearchRepository.kt index fd6b66bf9b..944b75914e 100644 --- a/form/src/main/java/org/dhis2/form/data/SearchRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/SearchRepository.kt @@ -1,6 +1,7 @@ package org.dhis2.form.data import io.reactivex.Flowable +import org.dhis2.form.data.metadata.FormBaseConfiguration import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.ui.FieldViewModelFactory @@ -14,7 +15,7 @@ class SearchRepository( override val programUid: String?, private val teiTypeUid: String, private val currentSearchValues: Map, -) : DataEntryBaseRepository(d2, fieldViewModelFactory) { +) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldViewModelFactory) { override fun list(): Flowable> { return programUid?.let { diff --git a/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt new file mode 100644 index 0000000000..1e89339500 --- /dev/null +++ b/form/src/main/java/org/dhis2/form/data/metadata/EnrollmentConfiguration.kt @@ -0,0 +1,123 @@ +package org.dhis2.form.data.metadata + +import org.dhis2.bindings.userFriendlyValue +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.enrollmentImportConflicts +import org.dhis2.commons.bindings.program +import org.dhis2.commons.bindings.tei +import org.dhis2.commons.bindings.teiAttribute +import org.dhis2.commons.bindings.trackedEntityType +import org.dhis2.form.model.OptionSetConfiguration +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit + +class EnrollmentConfiguration(private val d2: D2, private val enrollmentUid: String) : + FormBaseConfiguration(d2) { + private val _enrollment: Enrollment? by lazy { + d2.enrollment(enrollmentUid) + } + + fun enrollment() = _enrollment + + fun program() = enrollment()?.program()?.let { + d2.program(it) + } + + fun tei() = enrollment()?.trackedEntityInstance()?.let { d2.tei(it) } + fun trackedEntityType() = d2.trackedEntityType(program()?.trackedEntityType()?.uid()!!) + fun sections() = d2.programModule().programSections() + .withAttributes() + .byProgramUid().eq(enrollment()?.program()) + .blockingGet() + + fun programAttributes() = + d2.programModule().programTrackedEntityAttributes() + .withRenderType() + .byProgram().eq(enrollment()?.program()) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) + .blockingGet() + + fun programAttribute(attributeUid: String) = + d2.programModule().programTrackedEntityAttributes().withRenderType() + .byProgram().eq(enrollment()?.program()) + .byTrackedEntityAttribute().eq(attributeUid) + .one().blockingGet() + + fun trackedEntityAttribute(trackedEntityAttributeUid: String) = + d2.teiAttribute(trackedEntityAttributeUid) + + fun attributeValue(trackedEntityAttributeUid: String) = + d2.trackedEntityModule().trackedEntityAttributeValues() + .value( + trackedEntityAttributeUid, + enrollment()?.trackedEntityInstance()!!, + ).blockingGet()?.userFriendlyValue(d2) + + fun conflicts() = d2.enrollmentImportConflicts(enrollmentUid) + + fun fetchAutogeneratedValue(trackedEntityAttributeUid: String, orgUnitUid: String) = + d2.trackedEntityModule().reservedValueManager() + .blockingGetValue(trackedEntityAttributeUid, orgUnitUid) + + fun captureOrgUnitsCount() = d2.organisationUnitModule().organisationUnits() + .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) + .byProgramUids(enrollment()?.program()?.let { listOf(it) } ?: emptyList()) + .blockingCount() + + fun hasEventsGeneratedByEnrollmentDate(): Boolean { + val stagesWithReportDateToUse = d2.programModule().programStages() + .byProgramUid().eq(enrollment()?.program()) + .byOpenAfterEnrollment().isTrue + .byReportDateToUse().eq("enrollmentDate") + .blockingGetUids() + val stagesWithGeneratedBy = d2.programModule().programStages() + .byProgramUid().eq(enrollment()?.program()) + .byAutoGenerateEvent().isTrue + .byGeneratedByEnrollmentDate().isTrue + .blockingGetUids() + return !d2.eventModule().events() + .byEnrollmentUid().eq(enrollmentUid) + .byProgramStageUid().`in`(stagesWithReportDateToUse.union(stagesWithGeneratedBy)) + .blockingIsEmpty() + } + + fun hasEventsGeneratedByIncidentDate(): Boolean { + val stagesWithReportDateToUse = d2.programModule().programStages() + .byProgramUid().eq(enrollment()?.program()) + .byOpenAfterEnrollment().isTrue + .byReportDateToUse().eq("incidentDate") + .blockingGetUids() + val stagesWithGeneratedBy = d2.programModule().programStages() + .byProgramUid().eq(enrollment()?.program()) + .byAutoGenerateEvent().isTrue + .byGeneratedByEnrollmentDate().isFalse + .blockingGetUids() + return !d2.eventModule().events() + .byEnrollmentUid().eq(enrollmentUid) + .byProgramStageUid().`in`(stagesWithReportDateToUse.union(stagesWithGeneratedBy)) + .blockingIsEmpty() + } + + fun setValue(attributeUid: String, value: String) { + d2.trackedEntityModule().trackedEntityAttributeValues() + .value(attributeUid, tei()?.uid()!!) + .blockingSet(value) + } + + fun getValue(attributeUid: String) = d2.trackedEntityModule().trackedEntityAttributeValues() + .value(attributeUid, tei()?.uid()!!) + .blockingGet() + + fun optionSetConfig(optionSetUid: String) = + d2.optionModule().options().byOptionSetUid().eq(optionSetUid).blockingCount() + .let { optionCount -> + OptionSetConfiguration.config(optionCount) { + d2.optionModule().options() + .byOptionSetUid().eq(optionSetUid) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC) + .blockingGet() + } + } +} diff --git a/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt new file mode 100644 index 0000000000..d24fb47498 --- /dev/null +++ b/form/src/main/java/org/dhis2/form/data/metadata/FormBaseConfiguration.kt @@ -0,0 +1,14 @@ +package org.dhis2.form.data.metadata + +import org.dhis2.commons.bindings.disableCollapsableSectionsInProgram +import org.hisp.dhis.android.core.D2 + +open class FormBaseConfiguration(private val d2: D2) { + fun optionGroups(optionGroupUids: List) = d2.optionModule().optionGroups() + .withOptions() + .byUid().`in`(optionGroupUids) + .blockingGet() + + fun disableCollapsableSectionsInProgram(programUid: String) = + d2.disableCollapsableSectionsInProgram(programUid) +} diff --git a/form/src/main/java/org/dhis2/form/di/Injector.kt b/form/src/main/java/org/dhis2/form/di/Injector.kt index 58b45a292a..b2fec9542f 100644 --- a/form/src/main/java/org/dhis2/form/di/Injector.kt +++ b/form/src/main/java/org/dhis2/form/di/Injector.kt @@ -21,6 +21,7 @@ import org.dhis2.form.data.RuleEngineRepository import org.dhis2.form.data.RulesUtilsProviderImpl import org.dhis2.form.data.SearchOptionSetOption import org.dhis2.form.data.SearchRepository +import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.data.metadata.FileResourceConfiguration import org.dhis2.form.data.metadata.OptionSetConfiguration import org.dhis2.form.data.metadata.OrgUnitConfiguration @@ -157,8 +158,7 @@ object Injector { enrollmentRecords.allowMandatoryFields, enrollmentRecords.isBackgroundTransparent, ), - enrollmentUid = enrollmentRecords.enrollmentUid, - d2 = provideD2(), + conf = EnrollmentConfiguration(provideD2(), enrollmentRecords.enrollmentUid), enrollmentMode = enrollmentRecords.enrollmentMode, enrollmentFormLabelsProvider = provideEnrollmentFormLabelsProvider(context), ) diff --git a/form/src/main/java/org/dhis2/form/extensions/FieldUiModelExtensions.kt b/form/src/main/java/org/dhis2/form/extensions/FieldUiModelExtensions.kt index dd057677c5..56a1bc74e9 100644 --- a/form/src/main/java/org/dhis2/form/extensions/FieldUiModelExtensions.kt +++ b/form/src/main/java/org/dhis2/form/extensions/FieldUiModelExtensions.kt @@ -31,7 +31,7 @@ fun FieldUiModel.supportingText() = listOfNotNull( ).ifEmpty { null } fun FieldUiModel.legend() = legend?.let { - LegendData(Color(it.color), it.label ?: "", null) + LegendData(Color(it.color), it.label ?: "", it.legendsInfo) } fun FieldUiModel.orientation() = when (renderingType) { diff --git a/form/src/main/java/org/dhis2/form/model/LegendValue.kt b/form/src/main/java/org/dhis2/form/model/LegendValue.kt index 58e7c4df18..68559c41e1 100644 --- a/form/src/main/java/org/dhis2/form/model/LegendValue.kt +++ b/form/src/main/java/org/dhis2/form/model/LegendValue.kt @@ -1,3 +1,9 @@ package org.dhis2.form.model -data class LegendValue(val color: Int, val label: String?) +import org.hisp.dhis.mobile.ui.designsystem.component.LegendDescriptionData + +data class LegendValue( + val color: Int, + val label: String?, + val legendsInfo: List?, +) diff --git a/form/src/main/java/org/dhis2/form/ui/Form.kt b/form/src/main/java/org/dhis2/form/ui/Form.kt index 81fa048149..c4b7584871 100644 --- a/form/src/main/java/org/dhis2/form/ui/Form.kt +++ b/form/src/main/java/org/dhis2/form/ui/Form.kt @@ -6,13 +6,19 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -21,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.Color @@ -29,13 +36,19 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.dhis2.commons.resources.ResourceManager +import org.dhis2.form.R import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.FormSection import org.dhis2.form.ui.event.RecyclerViewUiEvents import org.dhis2.form.ui.intent.FormIntent import org.dhis2.form.ui.provider.inputfield.FieldProvider +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBar +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBarData import org.hisp.dhis.mobile.ui.designsystem.component.Section import org.hisp.dhis.mobile.ui.designsystem.component.SectionState +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @OptIn(ExperimentalFoundationApi::class) @Composable @@ -59,20 +72,20 @@ fun Form( } } } - val focusNext = remember { mutableStateOf(false) } - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - .clickable( - interactionSource = MutableInteractionSource(), - indication = null, - onClick = { focusManager.clearFocus() }, - ), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), - state = scrollState, - ) { - if (sections.isNotEmpty()) { + if (sections.isNotEmpty()) { + val focusNext = remember { mutableStateOf(false) } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .clickable( + interactionSource = MutableInteractionSource(), + indication = null, + onClick = { focusManager.clearFocus() }, + ), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), + state = scrollState, + ) { this.itemsIndexed( items = sections, key = { _, fieldUiModel -> fieldUiModel.uid }, @@ -100,48 +113,76 @@ fun Form( Section( title = section.title, isLastSection = getNextSection(section, sections) == null, - description = section.description, + description = if (sections.size >= 2 && section.fields.isNotEmpty()) section.description else null, completedFields = section.completedFields(), totalFields = section.fields.size, state = section.state, errorCount = section.errorCount(), warningCount = section.warningCount(), + warningMessage = resources.getString(R.string.form_without_fields), onNextSection = onNextSection, onSectionClick = { intentHandler.invoke(FormIntent.OnSection(section.uid)) }, content = { - section.fields.forEachIndexed { index, fieldUiModel -> - fieldUiModel.setCallback(callback) - FieldProvider( - modifier = Modifier.animateItemPlacement( - animationSpec = tween( - durationMillis = 500, - easing = LinearOutSlowInEasing, + if (sections.size >= 2 && section.fields.isNotEmpty()) { + section.fields.forEachIndexed { index, fieldUiModel -> + fieldUiModel.setCallback(callback) + FieldProvider( + modifier = Modifier.animateItemPlacement( + animationSpec = tween( + durationMillis = 500, + easing = LinearOutSlowInEasing, + ), ), - ), - fieldUiModel = fieldUiModel, - uiEventHandler = uiEventHandler, - intentHandler = intentHandler, - resources = resources, - focusManager = focusManager, - onNextClicked = { - if (index == section.fields.size - 1) { - onNextSection() - focusNext.value = true - } else { - focusManager.moveFocus(FocusDirection.Down) - } - }, - ) + fieldUiModel = fieldUiModel, + uiEventHandler = uiEventHandler, + intentHandler = intentHandler, + resources = resources, + focusManager = focusManager, + onNextClicked = { + if (index == section.fields.size - 1) { + onNextSection() + focusNext.value = true + } else { + focusManager.moveFocus(FocusDirection.Down) + } + }, + ) + } } }, ) } item(sections.size - 1) { - Spacer(modifier = Modifier.height(120.dp)) + Spacer(modifier = Modifier.height(Spacing.Spacing120)) } } + } else { + Column( + modifier = Modifier + .padding(Spacing.Spacing16), + ) { + InfoBar( + infoBarData = InfoBarData( + text = resources.getString(R.string.form_without_fields), + icon = { + Icon( + imageVector = Icons.Outlined.ErrorOutline, + contentDescription = "no fields", + tint = SurfaceColor.Warning, + ) + }, + color = SurfaceColor.Warning, + backgroundColor = SurfaceColor.WarningContainer, + actionText = null, + onClick = null, + ), + modifier = Modifier + .clip(shape = RoundedCornerShape(Radius.Full)) + .background(SurfaceColor.WarningContainer), + ) + } } } diff --git a/form/src/main/java/org/dhis2/form/ui/mapper/FormSectionMapper.kt b/form/src/main/java/org/dhis2/form/ui/mapper/FormSectionMapper.kt index 06f12753f1..0b0b02a171 100644 --- a/form/src/main/java/org/dhis2/form/ui/mapper/FormSectionMapper.kt +++ b/form/src/main/java/org/dhis2/form/ui/mapper/FormSectionMapper.kt @@ -29,16 +29,6 @@ class FormSectionMapper { ) } } - } else { - sections.add( - FormSection( - uid = "DUMMY", - title = "TITLE", - description = null, - state = SectionState.NO_HEADER, - fields = items.filterIsInstance(), - ), - ) } return sections diff --git a/form/src/main/java/org/dhis2/form/ui/provider/LegendValueProviderImpl.kt b/form/src/main/java/org/dhis2/form/ui/provider/LegendValueProviderImpl.kt index 2804ad40a2..11cef02226 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/LegendValueProviderImpl.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/LegendValueProviderImpl.kt @@ -1,8 +1,10 @@ package org.dhis2.form.ui.provider +import androidx.compose.ui.graphics.Color import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.model.LegendValue import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.mobile.ui.designsystem.component.LegendDescriptionData class LegendValueProviderImpl( val d2: D2, @@ -30,10 +32,25 @@ class LegendValueProviderImpl( .byLegendSet().eq(legendSet.uid()) .one() .blockingGet() + val legendValues = d2.legendSetModule() + .legendSets() + .withLegends() + .uid(legendSet.uid()) + .blockingGet() if (legend != null) { return LegendValue( resourceManager.getColorFrom(legend.color()), legend.displayName(), + legendValues?.legends()?.sortedBy { it.startValue() }?.map { + LegendDescriptionData( + Color(resourceManager.getColorFrom(it.color())), + it.displayName() ?: "", + IntRange( + it.startValue()?.toInt() ?: 0, + it.endValue()?.toInt() ?: 0, + ), + ) + }, ) } } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt index 59fe4cc758..84333f9865 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt @@ -27,7 +27,7 @@ internal fun ProvideCheckBoxInput( uid = option.uid(), checked = fieldUiModel.displayName == option.displayName(), enabled = true, - textInput = option.displayName(), + textInput = option.displayName() ?: "", ) } ?: emptyList() diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt index 2d79a341e3..3083b5dfc3 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt @@ -8,6 +8,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import org.dhis2.commons.date.DateUtils import org.dhis2.commons.extensions.toDate import org.dhis2.form.extensions.inputState @@ -36,14 +38,21 @@ fun ProvideInputDate( ValueType.TIME -> DateTimeActionIconType.TIME to TimeTransformation() else -> DateTimeActionIconType.DATE to DateTransformation() } + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value?.let { formatStoredDateToUI(it, fieldUiModel.valueType) }) + mutableStateOf( + if (fieldUiModel.value != null) { + TextFieldValue(formatStoredDateToUI(fieldUiModel.value!!, fieldUiModel.valueType), textSelection) + } else { + TextFieldValue() + }, + ) } InputDateTime( title = fieldUiModel.label, - value = value, + inputTextFieldValue = value, actionIconType = actionType, onActionClicked = { when (actionType) { @@ -51,7 +60,7 @@ fun ProvideInputDate( RecyclerViewUiEvents.OpenCustomCalendar( uid = fieldUiModel.uid, label = fieldUiModel.label, - date = value?.toDate(), + date = value.text.toDate(), allowFutureDates = fieldUiModel.allowFutureDates ?: true, isDateTime = false, ), @@ -61,7 +70,7 @@ fun ProvideInputDate( RecyclerViewUiEvents.OpenTimePicker( uid = fieldUiModel.uid, label = fieldUiModel.label, - date = formatUIDateToStored(value, fieldUiModel.valueType)?.let { + date = formatUIDateToStored(value.text, fieldUiModel.valueType)?.let { DateUtils.timeFormat().parse(it) }, isDateTime = false, @@ -72,7 +81,7 @@ fun ProvideInputDate( RecyclerViewUiEvents.OpenCustomCalendar( uid = fieldUiModel.uid, label = fieldUiModel.label, - date = formatUIDateToStored(value, fieldUiModel.valueType)?.let { + date = formatUIDateToStored(value.text, fieldUiModel.valueType)?.let { DateUtils.databaseDateFormatNoSeconds().parse(it) }, allowFutureDates = fieldUiModel.allowFutureDates ?: true, @@ -81,7 +90,7 @@ fun ProvideInputDate( ) } }, - modifier = modifier.semantics { contentDescription = formatStoredDateToUI(value ?: "", fieldUiModel.valueType) }, + modifier = modifier.semantics { contentDescription = formatStoredDateToUI(value.text, fieldUiModel.valueType) }, state = fieldUiModel.inputState(), legendData = fieldUiModel.legend(), supportingText = fieldUiModel.supportingText(), @@ -94,7 +103,7 @@ fun ProvideInputDate( intentHandler.invoke( FormIntent.OnTextChange( uid = fieldUiModel.uid, - value = formatUIDateToStored(it, fieldUiModel.valueType), + value = formatUIDateToStored(it.text, fieldUiModel.valueType), valueType = fieldUiModel.valueType, allowFutureDates = fieldUiModel.allowFutureDates ?: true, ), diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt index 8039db46dc..625a51a7e5 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue import kotlinx.coroutines.launch import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.extensions.autocompleteList @@ -447,8 +449,10 @@ private fun ProvideIntegerPositive( focusManager: FocusManager, onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputPositiveInteger( @@ -457,15 +461,15 @@ private fun ProvideIntegerPositive( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -486,8 +490,10 @@ private fun ProvideIntegerPositiveOrZero( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputPositiveIntegerOrZero( @@ -496,15 +502,15 @@ private fun ProvideIntegerPositiveOrZero( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -525,8 +531,10 @@ private fun ProvidePercentage( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputPercentage( @@ -535,15 +543,15 @@ private fun ProvidePercentage( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -564,8 +572,10 @@ private fun ProvideNumber( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputNumber( @@ -574,15 +584,15 @@ private fun ProvideNumber( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -604,8 +614,9 @@ private fun ProvideIntegerNegative( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value?.replace("-", "")) + mutableStateOf(TextFieldValue(fieldUiModel.value?.replace("-", "") ?: "", textSelection)) } InputNegativeInteger( @@ -614,15 +625,15 @@ private fun ProvideIntegerNegative( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -643,8 +654,10 @@ private fun ProvideLongText( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputLongText( @@ -653,15 +666,15 @@ private fun ProvideLongText( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -683,8 +696,9 @@ private fun ProvideLetter( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputLetter( @@ -693,15 +707,15 @@ private fun ProvideLetter( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -722,8 +736,9 @@ private fun ProvideInteger( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputInteger( @@ -732,15 +747,15 @@ private fun ProvideInteger( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -761,8 +776,10 @@ private fun ProvideEmail( focusManager: FocusManager, onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputEmail( @@ -771,15 +788,15 @@ private fun ProvideEmail( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -788,7 +805,7 @@ private fun ProvideEmail( uiEventHandler.invoke( RecyclerViewUiEvents.OpenChooserIntent( Intent.ACTION_SENDTO, - value, + value.text, fieldUiModel.uid, ), ) @@ -810,8 +827,10 @@ private fun ProvideInputPhoneNumber( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputPhoneNumber( @@ -820,15 +839,15 @@ private fun ProvideInputPhoneNumber( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -837,7 +856,7 @@ private fun ProvideInputPhoneNumber( uiEventHandler.invoke( RecyclerViewUiEvents.OpenChooserIntent( Intent.ACTION_DIAL, - value, + value.text, fieldUiModel.uid, ), ) @@ -859,8 +878,10 @@ private fun ProvideInputLink( onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputLink( @@ -869,15 +890,15 @@ private fun ProvideInputLink( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -886,7 +907,7 @@ private fun ProvideInputLink( uiEventHandler.invoke( RecyclerViewUiEvents.OpenChooserIntent( Intent.ACTION_VIEW, - value, + value.text, fieldUiModel.uid, ), ) diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt index e51321a7bb..9d069fdea0 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt @@ -8,6 +8,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import org.dhis2.form.extensions.autocompleteList import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend @@ -71,8 +73,9 @@ private fun ProvideQRInput( focusManager: FocusManager, onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputQRCode( @@ -81,21 +84,21 @@ private fun ProvideQRInput( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) }, onQRButtonClicked = { - if (value.isNullOrEmpty()) { + if (value.text.isEmpty()) { uiEventHandler.invoke( RecyclerViewUiEvents.ScanQRCode( fieldUiModel.uid, @@ -108,7 +111,7 @@ private fun ProvideQRInput( RecyclerViewUiEvents.DisplayQRCode( fieldUiModel.uid, optionSet = fieldUiModel.optionSet, - value = value!!, + value = value.text, renderingType = fieldUiModel.renderingType, editable = fieldUiModel.editable, label = fieldUiModel.label, @@ -132,7 +135,7 @@ private fun ProvideDefaultTextInput( onNextClicked: () -> Unit, ) { var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "")) } InputText( modifier = modifier.fillMaxWidth(), @@ -140,15 +143,15 @@ private fun ProvideDefaultTextInput( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) @@ -169,8 +172,10 @@ private fun ProvideBarcodeInput( focusManager: FocusManager, onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputBarCode( @@ -179,21 +184,21 @@ private fun ProvideBarcodeInput( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) }, onActionButtonClicked = { - if (value.isNullOrEmpty()) { + if (value.text.isEmpty()) { uiEventHandler.invoke( RecyclerViewUiEvents.ScanQRCode( fieldUiModel.uid, @@ -206,7 +211,7 @@ private fun ProvideBarcodeInput( RecyclerViewUiEvents.DisplayQRCode( fieldUiModel.uid, optionSet = fieldUiModel.optionSet, - value = value!!, + value = value.text, renderingType = fieldUiModel.renderingType, editable = fieldUiModel.editable, label = fieldUiModel.label, diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt index 1bc114e682..3bfe01c0d8 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt @@ -26,7 +26,7 @@ internal fun ProvideRadioButtonInput( uid = option.uid(), selected = fieldUiModel.displayName == option.displayName(), enabled = true, - textInput = option.displayName(), + textInput = option.displayName() ?: "", ) } ?: emptyList() diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt index 9c4558ae90..eb7dfa93f8 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt @@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -21,8 +23,10 @@ fun ProvideUnitIntervalInput( intentHandler: (FormIntent) -> Unit, onNextClicked: () -> Unit, ) { + val textSelection = TextRange(if (fieldUiModel.value != null) fieldUiModel.value!!.length else 0) + var value by remember(fieldUiModel.value) { - mutableStateOf(fieldUiModel.value) + mutableStateOf(TextFieldValue(fieldUiModel.value ?: "", textSelection)) } InputUnitInterval( modifier = modifier.fillMaxWidth(), @@ -30,15 +34,15 @@ fun ProvideUnitIntervalInput( state = fieldUiModel.inputState(), supportingText = fieldUiModel.supportingText(), legendData = fieldUiModel.legend(), - inputText = value ?: "", + inputTextFieldValue = value, isRequiredField = fieldUiModel.mandatory, onNextClicked = onNextClicked, onValueChanged = { - value = it + value = it ?: TextFieldValue() intentHandler( FormIntent.OnTextChange( fieldUiModel.uid, - value, + value.text, fieldUiModel.valueType, ), ) diff --git a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt index a006c8e012..c9c37647ea 100644 --- a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt +++ b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt @@ -44,6 +44,7 @@ class FormRepositoryImplTest { @Before fun setUp() { whenever(dataEntryRepository.disableCollapsableSections()) doReturn null + whenever(dataEntryRepository.firstSectionToOpen())doReturn mockedSections().first() whenever(dataEntryRepository.sectionUids()) doReturn Flowable.just(mockedSections()) whenever(dataEntryRepository.list()) doReturn Flowable.just(provideItemList()) repository = FormRepositoryImpl( diff --git a/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt b/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt new file mode 100644 index 0000000000..588888961a --- /dev/null +++ b/form/src/test/java/org/dhis2/form/data/FormRepositoryIntegrationTest.kt @@ -0,0 +1,192 @@ +package org.dhis2.form.data + +import org.dhis2.form.data.metadata.EnrollmentConfiguration +import org.dhis2.form.model.EnrollmentMode +import org.dhis2.form.model.SectionUiModelImpl +import org.dhis2.form.ui.FieldViewModelFactoryImpl +import org.dhis2.form.ui.provider.AutoCompleteProvider +import org.dhis2.form.ui.provider.DisplayNameProvider +import org.dhis2.form.ui.provider.EnrollmentFormLabelsProvider +import org.dhis2.form.ui.provider.HintProvider +import org.dhis2.form.ui.provider.KeyboardActionProvider +import org.dhis2.form.ui.provider.LayoutProvider +import org.dhis2.form.ui.provider.LegendValueProvider +import org.dhis2.form.ui.provider.UiEventTypesProvider +import org.dhis2.form.ui.provider.UiStyleProvider +import org.dhis2.form.ui.validation.FieldErrorMessageProvider +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Date + +class FormRepositoryIntegrationTest { + private val rulesUtilsProvider: RulesUtilsProvider = mock() + private val ruleEngineRepository: RuleEngineRepository = mock() + private val formValueStore: FormValueStore = mock() + private val fieldErrorMessageProvider: FieldErrorMessageProvider = mock() + private val conf: EnrollmentConfiguration = mock() + private val enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider = mock { + on { provideEnrollmentOrgUnitLabel() } doReturn "OrgUnit label" + } + + private val program: Program = mock { + on { description() } doReturn "program description" + on { enrollmentDateLabel() } doReturn "enrollment date label" + on { selectEnrollmentDatesInFuture() } doReturn false + on { displayIncidentDate() } doReturn false + on { access() } doReturn mock() + on { access().data() } doReturn mock() + on { access().data().write() } doReturn true + on { featureType() } doReturn FeatureType.NONE + } + + private val teType: TrackedEntityType = mock { + on { access() } doReturn mock() + on { access().data() } doReturn mock() + on { access().data().write() } doReturn true + on { featureType() } doReturn FeatureType.NONE + } + + @Before + fun setUp() { + whenever(conf.sections()) doReturn emptyList() + val programAttribute: ProgramTrackedEntityAttribute = mock { + on { trackedEntityAttribute() } doReturn ObjectWithUid.create("teAttributeUid") + on { mandatory() } doReturn false + } + whenever(conf.programAttributes()) doReturn listOf(programAttribute) + val teAttribute: TrackedEntityAttribute = mock { + on { uid() } doReturn "teAttributeUid" + on { valueType() } doReturn ValueType.TEXT + on { optionSet() } doReturn null + on { generated() } doReturn false + on { style() } doReturn ObjectStyle.builder().build() + on { fieldMask() } doReturn null + } + whenever(conf.trackedEntityAttribute("teAttributeUid")) doReturn teAttribute + whenever(conf.attributeValue(any())) doReturn null + whenever(conf.conflicts()) doReturn emptyList() + whenever(conf.program()) doReturn program + whenever(conf.trackedEntityType()) doReturn teType + whenever(conf.captureOrgUnitsCount()) doReturn 1 + + whenever(enrollmentFormLabelsProvider.provideSingleSectionLabel()) doReturn "single section label" + } + + @Test + fun shouldOpenEnrollmentDetailSectionIfIsNewAndNotCompleted() { + mockUncompletedEnrollment() + whenever(conf.disableCollapsableSectionsInProgram(any())) doReturn false + + val repository = mockFormRepository() + + val fields = repository.fetchFormItems() + assertTrue((fields.first { it.isSection() } as SectionUiModelImpl).isOpen == true) + } + + @Test + fun shouldOpenEnrollmentDetailSectionIfIsNewAndCompleted() { + mockCompletedEnrollment() + whenever(conf.disableCollapsableSectionsInProgram(any())) doReturn false + + val repository = mockFormRepository(EnrollmentMode.NEW) + + val fields = repository.fetchFormItems() + assertTrue((fields.first { it.isSection() } as SectionUiModelImpl).isOpen == true) + } + + @Test + fun shouldOpenEnrollmentDetailSectionIfNotCompleted() { + mockUncompletedEnrollment() + whenever(conf.disableCollapsableSectionsInProgram(any())) doReturn false + + val repository = mockFormRepository(EnrollmentMode.CHECK) + + val fields = repository.fetchFormItems() + assertTrue((fields.first { it.isSection() } as SectionUiModelImpl).isOpen == true) + } + + @Test + fun shouldNotOpenEnrollmentDetailSectionIfCompleted() { + mockCompletedEnrollment() + whenever(conf.disableCollapsableSectionsInProgram(any())) doReturn false + + val repository = mockFormRepository(EnrollmentMode.CHECK) + + val fields = repository.fetchFormItems() + assertTrue( + (fields.filter { it.isSection() }[1] as SectionUiModelImpl).isOpen == true, + ) + } + + private fun mockUncompletedEnrollment() { + val enrollment: Enrollment = mock { + on { enrollmentDate() } doReturn null + on { organisationUnit() } doReturn "orgUnitUid" + } + whenever(conf.enrollment()) doReturn enrollment + } + + private fun mockCompletedEnrollment() { + val enrollment: Enrollment = mock { + on { enrollmentDate() } doReturn Date() + on { organisationUnit() } doReturn "orgUnitUid" + } + whenever(conf.enrollment()) doReturn enrollment + } + + private fun mockFormRepository(enrollmentMode: EnrollmentMode = EnrollmentMode.NEW): FormRepositoryImpl { + val styleProvider: UiStyleProvider = mock() + val layoutProvider: LayoutProvider = mock() + val hintProvider: HintProvider = mock() + val displayNameProvider: DisplayNameProvider = mock() + val uiEventTypesProvider: UiEventTypesProvider = mock() + val keyboardActionProvider: KeyboardActionProvider = mock() + val legendValueProvider: LegendValueProvider = mock() + val autoCompleteProvider: AutoCompleteProvider = mock() + + val fieldFactory = FieldViewModelFactoryImpl( + false, + styleProvider, + layoutProvider, + hintProvider, + displayNameProvider, + uiEventTypesProvider, + keyboardActionProvider, + legendValueProvider, + autoCompleteProvider, + ) + + val dataEntryRepository = EnrollmentRepository( + fieldFactory, + conf, + enrollmentMode, + enrollmentFormLabelsProvider, + ) + + val repository = FormRepositoryImpl( + formValueStore, + fieldErrorMessageProvider, + displayNameProvider, + dataEntryRepository, + ruleEngineRepository, + rulesUtilsProvider, + legendValueProvider, + false, + ) + return repository + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5173b968ea..e0cfeb77e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" -vCode = "129" +vCode = "130" vName = "2.10-DEV" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.0" @@ -10,8 +10,8 @@ kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "0.2-20240123.112704-7" -dhis2sdk = "1.10.0-20240119.100209-6" +designSystem = "0.2-20240212.145552-20" +dhis2sdk = "1.10.0-20240129.132841-8" ruleEngine = "2.1.9" appcompat = "1.6.1" annotation = "1.6.0" 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 7539542dd2..d4e85b7d86 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,6 +62,8 @@ class HomeActivity : AppCompatActivity() { setContent { val settingsUiState by viewModel.settingsUiState.collectAsState() + val helperText by viewModel.helperText.collectAsState() + manageStockViewModel.setHelperText(helperText) updateTheme(settingsUiState.selectedTransactionItem.type) manageStockViewModel.setThemeColor(Color(colorResource(themeColor).toArgb())) MdcTheme { 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 0e7f387a36..a51d25224c 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 @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import org.dhis2.android.rtsm.R import org.dhis2.android.rtsm.commons.Constants.INTENT_EXTRA_APP_CONFIG @@ -54,6 +55,9 @@ class HomeViewModel @Inject constructor( private val _settingsUiSate = MutableStateFlow(SettingsUiState(programUid = config.program, transactionItems = transactionItems)) val settingsUiState: StateFlow = _settingsUiSate + private val _helperText = MutableStateFlow(null) + val helperText = _helperText.asStateFlow() + init { loadFacilities() loadDestinations() @@ -100,6 +104,7 @@ class HomeViewModel @Inject constructor( .observeOn(schedulerProvider.ui()) .subscribe( { dataElement -> + _helperText.value = dataElement.description() transactionItems.find { it.type == TransactionType.CORRECTION }?.label = dataElement.displayName() ?: TransactionType.CORRECTION.name _settingsUiSate.update { currentUiState -> currentUiState.copy(transactionItems = transactionItems) diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt index 896dc1fda0..d0656a0366 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt @@ -116,6 +116,8 @@ class ManageStockViewModel @Inject constructor( val tableConfigurationState: StateFlow = _tableConfigurationState + private var inputHelperText: String? = null + init { configureRelays() } @@ -321,6 +323,7 @@ class ManageStockViewModel @Inject constructor( id = cell.id ?: "", mainLabel = itemName, secondaryLabels = mutableListOf(resources.getString(R.string.quantity)), + helperText = inputHelperText, currentValue = cell.value, keyboardInputType = KeyboardInputType.NumberPassword(), error = stockEntry?.errorMessage, @@ -591,6 +594,10 @@ class ManageStockViewModel @Inject constructor( _bottomSheetState.value = false } + fun setHelperText(text: String?) { + inputHelperText = text + } + private fun refreshTableConfiguration() = TableConfigurationState( overwrittenTableWidth = tableDimensionStore.getTableWidth(), overwrittenRowHeaderWidth = tableDimensionStore.getWidthForSection(), diff --git a/ui-components/src/main/res/values/strings.xml b/ui-components/src/main/res/values/strings.xml index d799973f57..818e763cdf 100644 --- a/ui-components/src/main/res/values/strings.xml +++ b/ui-components/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Cancel Search Clear all + This form has no fields configured %s error %s errors