From 386aca5709eeecd1bbb5082c6aef5965041b187d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 14:12:06 +0100 Subject: [PATCH 01/18] Simprints RAMP ACF-1 unsaved program stage missing chart value fixed --- .../ramp/data/FormHistoryChartRepository.kt | 42 ++++++++++------ .../data/FormHistoryChartRepositoryTest.kt | 50 ++++++++++++++++++- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt index a24df72273..5330650540 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt @@ -11,14 +11,13 @@ class FormHistoryChartRepository( private val eventUid: String, private val d2: D2, ) { - private val currentEvent by lazy { loadCurrentEvent() } private val followUpVisitEventsByProgramStage = mutableMapOf>() fun getChart( fieldUiModel: FieldUiModel, configs: List, ): FormHistoryChart? { - val currentEvent = currentEvent ?: return null + val currentEvent = loadCurrentEvent() ?: return null val chartConfig = configs.firstOrNull { config -> config.programId?.trim() == currentEvent.program() && @@ -76,6 +75,7 @@ class FormHistoryChartRepository( d2 .eventModule() .events() + .withTrackedEntityDataValues() .uid(eventUid) .blockingGet() @@ -83,20 +83,30 @@ class FormHistoryChartRepository( currentEvent: Event, followUpVisitProgramStageUid: String, ): List { - followUpVisitEventsByProgramStage[followUpVisitProgramStageUid]?.let { return it } - - val enrollmentUid = currentEvent.enrollment() ?: return emptyList() + val enrollmentUid = currentEvent.enrollment() val currentEventDate = currentEvent.displayDate() - - return d2 - .eventModule() - .events() - .withTrackedEntityDataValues() - .byEnrollmentUid() - .eq(enrollmentUid) - .byProgramStageUid() - .eq(followUpVisitProgramStageUid) - .blockingGet() + val queriedEvents = + if (enrollmentUid.isNullOrBlank()) { + emptyList() + } else { + followUpVisitEventsByProgramStage[followUpVisitProgramStageUid] + ?: d2 + .eventModule() + .events() + .withTrackedEntityDataValues() + .byEnrollmentUid() + .eq(enrollmentUid) + .byProgramStageUid() + .eq(followUpVisitProgramStageUid) + .blockingGet() + .also { followUpVisitEventsByProgramStage[followUpVisitProgramStageUid] = it } + } + val events = + (queriedEvents + currentEvent) + .associateBy { event -> event.uid() } + .values + + return events .filter { followUpEvent -> val followUpEventDate = followUpEvent.displayDate() followUpEvent.uid() == eventUid || @@ -108,7 +118,7 @@ class FormHistoryChartRepository( { followUpEvent -> followUpEvent.displayDate() ?: Date(0) }, { followUpEvent -> followUpEvent.uid() }, ), - ).also { followUpVisitEventsByProgramStage[followUpVisitProgramStageUid] = it } + ) } private fun Event.dataValuesByDataElement(): Map = diff --git a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt index db465f5509..0b36d0f2f8 100644 --- a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt +++ b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt @@ -88,6 +88,51 @@ class FormHistoryChartRepositoryTest { assertEquals(1, chart?.currentValueIndex) } + @Test + fun `getChart should plot current input value when current event not saved yet`() { + val currentEvent = + getEvent( + uid = CURRENT_EVENT_UID, + eventDate = Date(2_000), + dataValues = + listOf( + dataValue(VISIT_NUMBER_UID, "1"), + dataValue(DATA_ELEMENT_UID, "9.0"), + ), + ) + stubCurrentEvent(currentEvent) + stubFollowUpEvents( + listOf( + getEvent( + uid = "previous", + eventDate = Date(1_000), + dataValues = + listOf( + dataValue(VISIT_NUMBER_UID, "0"), + dataValue(DATA_ELEMENT_UID, "8.0"), + ), + ), + ), + ) + + val chart = + repository.getChart( + fieldUiModel = + FieldUiModelImpl( + uid = DATA_ELEMENT_UID, + value = "9.5", + label = "Weight", + valueType = ValueType.NUMBER, + optionSetConfiguration = null, + autocompleteList = null, + ), + configs = listOf(getConfig()), + ) + + assertEquals(listOf(8f, 9.5f, null, null), chart?.values) + assertEquals(1, chart?.currentValueIndex) + } + @Test fun `getChart should return null when field is not configured`() { stubCurrentEvent(getEvent(uid = CURRENT_EVENT_UID)) @@ -166,7 +211,7 @@ class FormHistoryChartRepositoryTest { repository.getChart(fieldUiModel, listOf(getConfig())) repository.getChart(fieldUiModel, listOf(getConfig())) - verify(currentEventRepository, times(1)).blockingGet() + verify(currentEventRepository, times(2)).blockingGet() verify(followUpEvents, times(1)).blockingGet() } @@ -181,7 +226,8 @@ class FormHistoryChartRepositoryTest { private fun stubCurrentEvent(event: Event) { whenever(d2.eventModule().events()) doReturn events - whenever(events.uid(CURRENT_EVENT_UID)) doReturn currentEventRepository + whenever(events.withTrackedEntityDataValues()) doReturn eventsWithDataValues + whenever(eventsWithDataValues.uid(CURRENT_EVENT_UID)) doReturn currentEventRepository whenever(currentEventRepository.blockingGet()) doReturn event } From 6346789e3a23ac41a2c7cf9abbf8cca55f2a75aa Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 15:06:25 +0100 Subject: [PATCH 02/18] Simprints RAMP ACF-1,39,40,41 stale deleted entries set to not show in history charts & tables --- .../ramp/data/EventHistoryTableRepository.kt | 2 + .../data/EventHistoryTableRepositoryTest.kt | 67 ++++++++++++++++- .../ramp/data/FormHistoryChartRepository.kt | 2 + .../data/FormHistoryChartRepositoryTest.kt | 72 ++++++++++++++++++- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt b/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt index d7b2f65410..c3187041f6 100644 --- a/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt +++ b/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt @@ -296,6 +296,8 @@ class EventHistoryTableRepository( .eq(tableContext.enrollmentUid) .byProgramStageUid() .eq(followUpVisitProgramStageUid) + .byDeleted() + .isFalse .blockingGet() } val events = diff --git a/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt b/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt index 35cc534220..52710d4eca 100644 --- a/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt +++ b/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt @@ -4,6 +4,7 @@ import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig import org.dhis2.commons.simprints.ramp.model.RampDatastoreConfig import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.filters.internal.BooleanFilterConnector import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope @@ -40,6 +41,8 @@ class EventHistoryTableRepositoryTest { private val enrollmentEvents: EventCollectionRepository = mock() private val programStageFilter: StringFilterConnector = mock() private val followUpEvents: EventCollectionRepository = mock() + private val deletedFilter: BooleanFilterConnector = mock() + private val activeFollowUpEvents: EventCollectionRepository = mock() private val programStageDataElements: ProgramStageDataElementCollectionRepository = mock() private val programStageDataElementsWithRenderType: ProgramStageDataElementCollectionRepository = mock() private val programStageDataElementProgramStageFilter: @@ -205,6 +208,60 @@ class EventHistoryTableRepositoryTest { ) } + @Test + fun `table should exclude locally deleted follow-up events`() { + stubRampConfig() + stubRowsAndSections() + stubFollowUpEvents( + events = + listOf( + event( + uid = "active", + eventDate = Date(1_000), + values = + listOf( + value(VISIT_NUMBER_UID, "0"), + value(WEIGHT_UID, "8.0"), + ), + ), + ), + unfilteredEvents = + listOf( + event( + uid = "deleted", + eventDate = Date(1_000), + values = + listOf( + value(VISIT_NUMBER_UID, "0"), + value(WEIGHT_UID, "99.0"), + ), + ).toBuilder().deleted(true).build(), + ), + ) + stubOptions() + + val table = + EventHistoryTableRepository( + d2 = d2, + simprintsRampDatastoreRepository = simprintsRampDatastoreRepository, + programUid = PROGRAM_UID, + enrollmentUid = ENROLLMENT_UID, + ).getTable() + + assertEquals("active", table?.columns?.get(0)?.eventUid) + assertEquals( + "8.0", + table + ?.sections + ?.single() + ?.rows + ?.first() + ?.values + ?.first() + ?.value, + ) + } + private fun stubRampConfig() { whenever(simprintsRampDatastoreRepository.getConfig()) doReturn RampDatastoreConfig( @@ -230,14 +287,20 @@ class EventHistoryTableRepositoryTest { whenever(currentEventRepository.blockingGet()) doReturn event } - private fun stubFollowUpEvents(events: List) { + private fun stubFollowUpEvents( + events: List, + unfilteredEvents: List = events, + ) { whenever(d2.eventModule().events()) doReturn this.events whenever(this.events.withTrackedEntityDataValues()) doReturn eventsWithDataValues whenever(eventsWithDataValues.byEnrollmentUid()) doReturn enrollmentFilter whenever(enrollmentFilter.eq(ENROLLMENT_UID)) doReturn enrollmentEvents whenever(enrollmentEvents.byProgramStageUid()) doReturn programStageFilter whenever(programStageFilter.eq(PROGRAM_STAGE_UID)) doReturn followUpEvents - whenever(followUpEvents.blockingGet()) doReturn events + whenever(followUpEvents.blockingGet()) doReturn unfilteredEvents + whenever(followUpEvents.byDeleted()) doReturn deletedFilter + whenever(deletedFilter.isFalse) doReturn activeFollowUpEvents + whenever(activeFollowUpEvents.blockingGet()) doReturn events } private fun stubRowsAndSections() { diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt index 5330650540..54050ba99f 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt @@ -98,6 +98,8 @@ class FormHistoryChartRepository( .eq(enrollmentUid) .byProgramStageUid() .eq(followUpVisitProgramStageUid) + .byDeleted() + .isFalse .blockingGet() .also { followUpVisitEventsByProgramStage[followUpVisitProgramStageUid] = it } } diff --git a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt index 0b36d0f2f8..a762bc2f7f 100644 --- a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt +++ b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt @@ -3,6 +3,7 @@ package org.dhis2.form.simprints.ramp.data import org.dhis2.commons.simprints.ramp.model.DataElementHistoryChartConfig import org.dhis2.form.model.FieldUiModelImpl import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.filters.internal.BooleanFilterConnector import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.event.Event @@ -29,6 +30,8 @@ class FormHistoryChartRepositoryTest { private val enrollmentEvents: EventCollectionRepository = mock() private val programStageFilter: StringFilterConnector = mock() private val followUpEvents: EventCollectionRepository = mock() + private val deletedFilter: BooleanFilterConnector = mock() + private val activeFollowUpEvents: EventCollectionRepository = mock() private val repository = FormHistoryChartRepository(CURRENT_EVENT_UID, d2) @Test @@ -185,6 +188,63 @@ class FormHistoryChartRepositoryTest { assertEquals(1, chart?.currentValueIndex) } + @Test + fun `getChart should exclude locally deleted follow-up events`() { + val currentEvent = + getEvent( + uid = CURRENT_EVENT_UID, + eventDate = Date(2_000), + dataValues = + listOf( + dataValue(VISIT_NUMBER_UID, "1"), + dataValue(DATA_ELEMENT_UID, "9.0"), + ), + ) + stubCurrentEvent(currentEvent) + stubFollowUpEvents( + eventsToReturn = + listOf( + getEvent( + uid = "active", + eventDate = Date(1_000), + dataValues = + listOf( + dataValue(VISIT_NUMBER_UID, "0"), + dataValue(DATA_ELEMENT_UID, "8.0"), + ), + ), + ), + unfilteredEvents = + listOf( + getEvent( + uid = "deleted", + eventDate = Date(1_000), + dataValues = + listOf( + dataValue(VISIT_NUMBER_UID, "0"), + dataValue(DATA_ELEMENT_UID, "99.0"), + ), + ).toBuilder().deleted(true).build(), + ), + ) + + val chart = + repository.getChart( + fieldUiModel = + FieldUiModelImpl( + uid = DATA_ELEMENT_UID, + value = "9.5", + label = "Weight", + valueType = ValueType.NUMBER, + optionSetConfiguration = null, + autocompleteList = null, + ), + configs = listOf(getConfig()), + ) + + assertEquals(listOf(8f, 9.5f, null, null), chart?.values) + } + @Test fun `getChart should reuse loaded current event and follow up events`() { val currentEvent = @@ -212,7 +272,7 @@ class FormHistoryChartRepositoryTest { repository.getChart(fieldUiModel, listOf(getConfig())) verify(currentEventRepository, times(2)).blockingGet() - verify(followUpEvents, times(1)).blockingGet() + verify(activeFollowUpEvents, times(1)).blockingGet() } private fun getConfig() = @@ -231,13 +291,19 @@ class FormHistoryChartRepositoryTest { whenever(currentEventRepository.blockingGet()) doReturn event } - private fun stubFollowUpEvents(eventsToReturn: List) { + private fun stubFollowUpEvents( + eventsToReturn: List, + unfilteredEvents: List = eventsToReturn, + ) { whenever(events.withTrackedEntityDataValues()) doReturn eventsWithDataValues whenever(eventsWithDataValues.byEnrollmentUid()) doReturn enrollmentFilter whenever(enrollmentFilter.eq(ENROLLMENT_UID)) doReturn enrollmentEvents whenever(enrollmentEvents.byProgramStageUid()) doReturn programStageFilter whenever(programStageFilter.eq(PROGRAM_STAGE_UID)) doReturn followUpEvents - whenever(followUpEvents.blockingGet()) doReturn eventsToReturn + whenever(followUpEvents.blockingGet()) doReturn unfilteredEvents + whenever(followUpEvents.byDeleted()) doReturn deletedFilter + whenever(deletedFilter.isFalse) doReturn activeFollowUpEvents + whenever(activeFollowUpEvents.blockingGet()) doReturn eventsToReturn } private fun getEvent( From 7de56f8e7ef7a891b0754a6395ace79952ccbe79 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 15:25:05 +0100 Subject: [PATCH 03/18] Simprints RAMP ACF-1 larger text in charts --- .../org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt index b7657764e6..6155396a12 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt @@ -37,7 +37,7 @@ import kotlin.math.roundToInt private const val HISTORY_CHART_HEIGHT_DP = 180 private const val HISTORY_CHART_PERIOD_STEP = 1L private const val HISTORY_CHART_X_AXIS_LABEL_ROTATION = 30f -private const val HISTORY_CHART_TEXT_SIZE_FACTOR = 1.25f +private const val HISTORY_CHART_TEXT_SIZE_FACTOR = 1.5f private const val HISTORY_CHART_TEXT_SIZE = DEFAULT_VALUE_TEXT_SIZE * HISTORY_CHART_TEXT_SIZE_FACTOR private const val HISTORY_CHART_Y_AXIS_LABEL_COUNT = 5 private const val HISTORY_CHART_FLAT_RANGE_EPSILON = 0.0001f From a8f2979e477abafaf498d747d19a678f966610bb Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 15:46:52 +0100 Subject: [PATCH 04/18] Simprints RAMP ACF-39,40,41 History table tab replaces the upstream Notes tab instead of following it in the tab row --- .../teidashboard/TeiDashboardTest.kt | 66 +------------------ .../teidashboard/robot/TeiDashboardRobot.kt | 8 +-- .../eventCapture/EventCaptureActivity.kt | 1 + .../eventCapture/EventCapturePagerAdapter.kt | 8 ++- .../eventCapture/EventPageConfigurator.kt | 2 +- .../teiDashboard/DashboardViewModel.kt | 18 ++--- .../TeiDashboardPageConfigurator.kt | 2 +- .../eventCapture/EventPageConfiguratorTest.kt | 5 ++ .../TeiDashboardPageConfiguratorTest.kt | 5 +- 9 files changed, 33 insertions(+), 82 deletions(-) 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 93c3c49e54..ebbeda6276 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -20,7 +20,6 @@ import org.dhis2.usescases.teidashboard.robot.analyticsRobot import org.dhis2.usescases.teidashboard.robot.enrollmentRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.indicatorsRobot -import org.dhis2.usescases.teidashboard.robot.noteRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Assume @@ -48,36 +47,7 @@ class TeiDashboardTest : BaseTest() { } @Test - fun shouldSuccessfullyCreateANoteWhenClickCreateNote() { - enableIntents() - mockWebServerRobot.addResponse( - method = ResponseController.GET, - path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, - sdkResource = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_RESPONSE, - responseCode = 200, - ) - - setupCredentials() - - prepareTeiCompletedProgrammeAndLaunchActivity(rule) - - teiDashboardRobot(composeTestRule) { - goToNotes() - } - - noteRobot { - clickOnFabAddNewNote() - verifyNoteDetailActivityIsLaunched() - typeNote(NOTE_VALID) - clickOnSaveButton() - checkNewNoteWasCreated(NOTE_VALID) - } - - } - - @Test - fun shouldNotCreateANoteWhenClickClear() { - enableIntents() + fun shouldNotDisplayNotesTab() { mockWebServerRobot.addResponse( method = ResponseController.GET, path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, @@ -88,39 +58,7 @@ class TeiDashboardTest : BaseTest() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) teiDashboardRobot(composeTestRule) { - goToNotes() - } - - noteRobot { - clickOnFabAddNewNote() - verifyNoteDetailActivityIsLaunched() - typeNote(NOTE_INVALID) - clickOnClearButton() - clickYesOnAlertDialog() - checkNoteWasNotCreated(NOTE_INVALID) - } - } - - @Test - fun shouldOpenNotesDetailsWhenClickOnNote() { - mockWebServerRobot.addResponse( - method = ResponseController.GET, - path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, - sdkResource = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_RESPONSE, - responseCode = 200, - ) - - prepareTeiWithExistingNoteAndLaunchActivity(rule) - - teiDashboardRobot(composeTestRule) { - goToNotes() - } - - noteRobot { - clickOnFabAddNewNote() - typeNote(NOTE_EXISTING_TEXT) - clickOnSaveButton() - checkNoteDetails("@$USER", NOTE_EXISTING_TEXT) + checkNotesTabDoesNotExist() } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt index 5c1ae78777..ee73a5e4f7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt @@ -60,13 +60,13 @@ fun teiDashboardRobot( class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun goToNotes() { + fun checkNotesTabDoesNotExist() { composeTestRule.onNodeWithText( InstrumentationRegistry.getInstrumentation().targetContext.getString( R.string.navigation_notes - ) - ).performClick() - waitToDebounce(500) + ), + useUnmergedTree = true, + ).assertDoesNotExist() } fun goToRelationships() { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt index 91a450ffd7..f524d26acc 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt @@ -170,6 +170,7 @@ class EventCaptureActivity : pageConfigurator!!.displayAnalytics(), pageConfigurator!!.displayTableView(), pageConfigurator!!.displayRelationships(), + pageConfigurator!!.displayNotes(), intent.getBooleanExtra(OPEN_ERROR_LOCATION, false), eventMode, ) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt index 6c3e60f8a3..1835278e99 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt @@ -22,6 +22,7 @@ class EventCapturePagerAdapter( displayAnalyticScreen: Boolean, displaySimprintsRampHistoryTableScreen: Boolean, displayRelationshipScreen: Boolean, + displayNotes: Boolean, private val shouldOpenErrorSection: Boolean, private val eventMode: EventMode, ) : FragmentStateAdapter(fragmentActivity) { @@ -59,8 +60,11 @@ class EventCapturePagerAdapter( portraitPages.add(EventPageType.RELATIONSHIPS) landscapePages.add(EventPageType.RELATIONSHIPS) } - portraitPages.add(EventPageType.NOTES) - landscapePages.add(EventPageType.NOTES) + + if (displayNotes) { + portraitPages.add(EventPageType.NOTES) + landscapePages.add(EventPageType.NOTES) + } if (displaySimprintsRampHistoryTableScreen) { portraitPages.add(EventPageType.SIMPRINTS_RAMP_HISTORY_TABLE) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt index 4e8fd182b8..e1c7e03095 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt @@ -14,7 +14,7 @@ class EventPageConfigurator( override fun displayRelationships(): Boolean = eventCaptureRepository.hasRelationships() - override fun displayNotes(): Boolean = true + override fun displayNotes(): Boolean = false override fun displayTableView(): Boolean = eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable() } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index eb0bdf3f4b..8b90d5d143 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -158,14 +158,16 @@ class DashboardViewModel( ) } - enrollmentItems.add( - NavigationBarItem( - id = TEIDashboardItems.NOTES, - icon = Icons.AutoMirrored.Outlined.StickyNote2, - selectedIcon = Icons.AutoMirrored.Filled.StickyNote2, - label = resourcesManager.getString(R.string.navigation_notes), - ), - ) + if (pageConfigurator.displayNotes()) { + enrollmentItems.add( + NavigationBarItem( + id = TEIDashboardItems.NOTES, + icon = Icons.AutoMirrored.Outlined.StickyNote2, + selectedIcon = Icons.AutoMirrored.Filled.StickyNote2, + label = resourcesManager.getString(R.string.navigation_notes), + ), + ) + } if (pageConfigurator.displayTableView()) { enrollmentItems.add( diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt index 23f524b7ed..9a383020dc 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt @@ -12,7 +12,7 @@ class TeiDashboardPageConfigurator( override fun displayRelationships(): Boolean = dashboardRepository.programHasRelationships() - override fun displayNotes(): Boolean = true + override fun displayNotes(): Boolean = false override fun displayTableView(): Boolean = dashboardRepository.programHasSimprintsRampProgramStageHistoryTable() } diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt index fe1d5213fc..611addff24 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt @@ -24,4 +24,9 @@ class EventPageConfiguratorTest { whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn false assertFalse(pageConfigurator.displayTableView()) } + + @Test + fun `displayNotes should be false`() { + assertFalse(pageConfigurator.displayNotes()) + } } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt index d435dd3833..191c62c189 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt @@ -1,6 +1,7 @@ package org.dhis2.usescases.teiDashboard import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.mockito.kotlin.doReturn @@ -43,8 +44,8 @@ class TeiDashboardPageConfiguratorTest { } @Test - fun `Should display the notes screen`() { - assertTrue(pageConfigurator.displayNotes()) + fun `Should not display the notes screen`() { + assertFalse(pageConfigurator.displayNotes()) } @Test From a5a6deaa49869206bb8b8d3871837a75379f9b79 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 17:29:52 +0100 Subject: [PATCH 05/18] Simprints RAMP ACF-39,40,41 Details tab added to the landscape tab row on TEI enrolment screen when History tab is selected and viewed full-width - for easier navigation --- .../teiDashboard/DashboardViewModel.kt | 41 +++++++++---- .../TeiDashboardMobileActivity.kt | 32 ++++++---- .../teiDashboard/DashboardViewModelTest.kt | 58 +++++++++++++++++++ 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index 8b90d5d143..9e1a451acb 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -32,7 +32,6 @@ import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.FOLLOW_UP import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator -import org.dhis2.utils.isPortrait import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.State.SYNCED import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -49,6 +48,7 @@ class DashboardViewModel( private val eventUid = MutableLiveData() private val selectedEventUid = MutableLiveData() + private var displayDetailsNavigationItem = pageConfigurator.displayDetails() val showStatusErrorMessages = MutableLiveData(StatusChangeResultCode.CHANGED) @@ -123,9 +123,13 @@ class DashboardViewModel( } private fun loadNavigationBarItems() { + updateNavigationBarItems(_navigationBarUIState.value.selectedItem) + } + + private fun updateNavigationBarItems(selectedItem: TEIDashboardItems?) { val enrollmentItems = mutableListOf>() - if (isPortrait()) { + if (displayDetailsNavigationItem || selectedItem == TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) { enrollmentItems.add( NavigationBarItem( id = TEIDashboardItems.DETAILS, @@ -180,15 +184,16 @@ class DashboardViewModel( ) } - _navigationBarUIState.value = _navigationBarUIState.value.copy(items = enrollmentItems) + val resolvedSelectedItem = + selectedItem + ?.takeIf { item -> enrollmentItems.any { it.id == item } } + ?: enrollmentItems.firstOrNull()?.id - if (navigationBarUIState.value.items.none { it.id == navigationBarUIState.value.selectedItem }) { - onNavigationItemSelected( - navigationBarUIState.value.items - .first() - .id, + _navigationBarUIState.value = + _navigationBarUIState.value.copy( + items = enrollmentItems, + selectedItem = resolvedSelectedItem, ) - } } private fun fetchGrouping() { @@ -301,9 +306,25 @@ class DashboardViewModel( } fun onNavigationItemSelected(itemId: TEIDashboardItems) { - _navigationBarUIState.value = _navigationBarUIState.value.copy(selectedItem = itemId) + updateNavigationBarItems(itemId) } + fun setDetailsNavigationItemVisible(visible: Boolean) { + if (displayDetailsNavigationItem != visible) { + displayDetailsNavigationItem = visible + updateNavigationBarItems(_navigationBarUIState.value.selectedItem) + } + } + + fun getDefaultLandscapeNavigationItemExceptHistory(): TEIDashboardItems? = + _navigationBarUIState + .value + .items + .firstOrNull { + it.id != TEIDashboardItems.DETAILS && // not in tab bar in landscape + it.id != TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE + }?.id + fun checkIfTeiCanBeTransferred(): Boolean = repository.teiCanBeTransferred() fun transferTei( 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 a0d604f464..58ec29e1b6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -15,7 +15,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoveDown import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -200,6 +199,7 @@ class TeiDashboardMobileActivity : super.onCreate(savedInstanceState) dashboardViewModel = ViewModelProvider(this, viewModelFactory)[DashboardViewModel::class.java] + dashboardViewModel.setDetailsNavigationItemVisible(isPortrait()) binding = DataBindingUtil.setContentView(this, R.layout.activity_dashboard_mobile) showLoadingProgress(true) binding.presenter = presenter @@ -340,20 +340,16 @@ class TeiDashboardMobileActivity : binding.navigationBar.setContent { DHIS2Theme { val uiState by dashboardViewModel.navigationBarUIState.collectAsStateWithLifecycle() - var selectedHomeItemIndex by remember(uiState) { - mutableIntStateOf( - uiState.items.indexOfFirst { - it.id == uiState.selectedItem - }, - ) - } + val selectedHomeItemIndex = + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + } NavigationBar( items = uiState.items, selectedItemIndex = selectedHomeItemIndex, ) { itemId -> - selectedHomeItemIndex = uiState.items.indexOfFirst { it.id == itemId } - dashboardViewModel.onNavigationItemSelected(itemId) + onNavigationBarItemSelected(itemId) } uiState.selectedItem?.let { @@ -363,6 +359,21 @@ class TeiDashboardMobileActivity : } } + private fun onNavigationBarItemSelected(item: TEIDashboardItems) { + if (isLandscape() && item == TEIDashboardItems.DETAILS) { + restoreSimprintsRampHistoryTableLandscapeLayoutAndSelectDefaultPage() + } else { + dashboardViewModel.onNavigationItemSelected(item) + } + } + + private fun restoreSimprintsRampHistoryTableLandscapeLayoutAndSelectDefaultPage() { + setSimprintsRampHistoryTableLandscapeFullscreen(false) + dashboardViewModel + .getDefaultLandscapeNavigationItemExceptHistory() + ?.let(dashboardViewModel::onNavigationItemSelected) + } + private fun navigateToFragment(item: TEIDashboardItems) { setSimprintsRampHistoryTableLandscapeFullscreen(item == TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) @@ -480,6 +491,7 @@ class TeiDashboardMobileActivity : super.onResume() if (sessionManagerServiceImpl.isUserLoggedIn()) { currentOrientation = if (this.isLandscape()) 1 else 0 + dashboardViewModel.setDetailsNavigationItemVisible(isPortrait()) presenter.refreshTabCounters() dashboardViewModel.updateDashboard() } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt index 48dd0e14fd..064b3d8ba0 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt @@ -13,9 +13,11 @@ import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.FOLLOW_UP import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.dhis2.tracker.TEIDashboardItems import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -40,6 +42,7 @@ class DashboardViewModelTest { @Before fun setUp() { Dispatchers.setMain(testingDispatcher) + whenever(resoourcesManager.getString(any())) doReturn "" } @Test @@ -101,6 +104,58 @@ class DashboardViewModelTest { } } + @Test + fun shouldDisplayDetailsWhenHistoryIsSelectedInLandscape() { + mockEnrollmentModel() + mockGrouping(false) + whenever(pageConfigurator.displayDetails()) doReturn false + whenever(pageConfigurator.displayAnalytics()) doReturn true + whenever(pageConfigurator.displayTableView()) doReturn true + + val dashboardViewModel = getViewModel() + + assertTrue( + dashboardViewModel.navigationItemIds() == + listOf( + TEIDashboardItems.ANALYTICS, + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + ), + ) + + dashboardViewModel.onNavigationItemSelected(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) + + assertTrue( + dashboardViewModel.navigationItemIds() == + listOf( + TEIDashboardItems.DETAILS, + TEIDashboardItems.ANALYTICS, + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + ), + ) + assertTrue( + dashboardViewModel.navigationBarUIState.value.selectedItem == + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + ) + + dashboardViewModel.onNavigationItemSelected(TEIDashboardItems.ANALYTICS) + + assertFalse(dashboardViewModel.navigationItemIds().contains(TEIDashboardItems.DETAILS)) + } + + @Test + fun shouldKeepDetailsVisibleInPortrait() { + mockEnrollmentModel() + mockGrouping(false) + whenever(pageConfigurator.displayDetails()) doReturn true + whenever(pageConfigurator.displayTableView()) doReturn true + + val dashboardViewModel = getViewModel() + + assertTrue(dashboardViewModel.navigationItemIds().contains(TEIDashboardItems.DETAILS)) + dashboardViewModel.onNavigationItemSelected(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) + assertTrue(dashboardViewModel.navigationItemIds().contains(TEIDashboardItems.DETAILS)) + } + @Test fun shouldSetFollowUpOnEnrollment() { mockEnrollmentModel() @@ -168,6 +223,9 @@ class DashboardViewModelTest { testingDispatcher.scheduler.advanceUntilIdle() } + private fun DashboardViewModel.navigationItemIds(): List = + navigationBarUIState.value.items.map { it.id } + private fun mockEnrollmentModel() { whenever(repository.getDashboardModel()) doReturn mockedEnrollmentModel whenever(mockedEnrollmentModel.currentEnrollment) doReturn mockedEnrollment From ae532a432a1c8513b69157dbfb2af4233d0b400c Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 18:00:13 +0100 Subject: [PATCH 06/18] Simprints RAMP infra: RAMPcapture Version info in About --- .../rampcapture-github-release-signed-apk.yml | 1 + app/build.gradle.kts | 2 ++ .../java/org/dhis2/usescases/about/AboutRobot.kt | 6 +++--- .../java/org/dhis2/usescases/about/AboutTest.kt | 15 +++++++++++---- .../java/org/dhis2/bindings/ContextExtensions.kt | 7 +++++++ .../org/dhis2/usescases/about/AboutFragment.kt | 3 +++ app/src/main/res/layout/fragment_about.xml | 10 +++++++++- app/src/main/res/values/strings.xml | 3 ++- 8 files changed, 38 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rampcapture-github-release-signed-apk.yml b/.github/workflows/rampcapture-github-release-signed-apk.yml index 1153174ad1..09f3054595 100644 --- a/.github/workflows/rampcapture-github-release-signed-apk.yml +++ b/.github/workflows/rampcapture-github-release-signed-apk.yml @@ -87,6 +87,7 @@ jobs: - name: Build signed release APK run: ./gradlew app:assembleDhis2Release env: + RAMP_CAPTURE_VERSION: ${{ steps.release-info.outputs.forkNumber }} SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index baeb1c83b8..ac791a9e9e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -93,8 +93,10 @@ android { multiDexEnabled = true val bitriseSentryDSN = System.getenv("SENTRY_DSN") ?: "" + val rampCaptureVersion = System.getenv("RAMP_CAPTURE_VERSION") ?: "local build" buildConfigField("String", "SDK_VERSION", "\"" + libs.versions.dhis2sdk.get() + "\"") + buildConfigField("String", "RAMP_CAPTURE_VERSION", "\"$rampCaptureVersion\"") buildConfigField("String", "MATOMO_URL", "\"https://usage.analytics.dhis2.org/matomo.php\"") buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") diff --git a/app/src/androidTest/java/org/dhis2/usescases/about/AboutRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/about/AboutRobot.kt index 26b675b31d..9e4df1b3d5 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/about/AboutRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/about/AboutRobot.kt @@ -15,9 +15,9 @@ fun aboutRobot(aboutBody: AboutRobot.() -> Unit) { class AboutRobot : BaseRobot() { - fun checkVersionNames(appName: String, sdkName: String) { - val appNameText = "App Version: $appName" - onView(withId(R.id.aboutApp)).check(matches(withText(appNameText))) + fun checkVersionNames(rampCaptureName: String, appName: String, sdkName: String) { + onView(withId(R.id.rampCaptureVersion)).check(matches(withText(rampCaptureName))) + onView(withId(R.id.aboutApp)).check(matches(withText(appName))) onView(withId(R.id.appSDK)).check(matches(withText(sdkName))) } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/about/AboutTest.kt b/app/src/androidTest/java/org/dhis2/usescases/about/AboutTest.kt index 2e3261c54e..b5beed52dd 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/about/AboutTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/about/AboutTest.kt @@ -3,6 +3,7 @@ package org.dhis2.usescases.about import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import org.dhis2.bindings.buildInfo +import org.dhis2.bindings.rampCaptureBuildInfo import org.dhis2.BuildConfig import org.dhis2.R import org.dhis2.usescases.BaseTest @@ -21,6 +22,7 @@ class AboutTest : BaseTest() { @Test fun shouldCheckVersionsWhenOpenAboutScreen() { startActivity() + val rampCaptureVersion = getRampCaptureVersionName() val appVersion = getAppVersionName() val sdkVersion = getSDKVersionName() @@ -30,7 +32,7 @@ class AboutTest : BaseTest() { } aboutRobot { - checkVersionNames(appVersion, sdkVersion) + checkVersionNames(rampCaptureVersion, appVersion, sdkVersion) } } @@ -38,9 +40,14 @@ class AboutTest : BaseTest() { rule.launchActivity(null) } - private fun getAppVersionName(): String { - return context.buildInfo() - } + private fun getRampCaptureVersionName() = + String.format( + context.getString(R.string.ramp_capture_version), + context.rampCaptureBuildInfo(), + ) + + private fun getAppVersionName() = + String.format(context.getString(R.string.about_app), context.buildInfo()) private fun getSDKVersionName() = String.format(context.getString(R.string.about_sdk), BuildConfig.SDK_VERSION) diff --git a/app/src/main/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/main/java/org/dhis2/bindings/ContextExtensions.kt index f423efd751..652401b5da 100644 --- a/app/src/main/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/main/java/org/dhis2/bindings/ContextExtensions.kt @@ -14,6 +14,13 @@ fun Context.buildInfo(): String = "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } +fun Context.rampCaptureBuildInfo(): String = + if (BuildConfig.BUILD_TYPE == "release") { + BuildConfig.RAMP_CAPTURE_VERSION + } else { + "${BuildConfig.RAMP_CAPTURE_VERSION} : ${BuildConfig.GIT_SHA} " + } + fun Fragment.checkSMSPermission( requestPermission: Boolean, request: Int, diff --git a/app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt b/app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt index a033100e3d..0ddbb19c63 100644 --- a/app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt @@ -11,6 +11,7 @@ import org.dhis2.BuildConfig import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.bindings.buildInfo +import org.dhis2.bindings.rampCaptureBuildInfo import org.dhis2.databinding.FragmentAboutBinding import org.dhis2.usescases.general.FragmentGlobalAbstract import org.hisp.dhis.android.core.user.User @@ -41,6 +42,8 @@ class AboutFragment : aboutGit.movementMethod = LinkMovementMethod.getInstance() aboutDev.movementMethod = LinkMovementMethod.getInstance() aboutContact.movementMethod = LinkMovementMethod.getInstance() + rampCaptureVersion.text = + getString(R.string.ramp_capture_version).format(context?.rampCaptureBuildInfo()) aboutApp.text = getString(R.string.about_app).format(context?.buildInfo()) appSDK.text = getString(R.string.about_sdk).format(BuildConfig.SDK_VERSION) privacyPolicy.setOnClickListener { navigateToPrivacyPolicy() } diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index a63fff7254..edbcd1bca7 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -28,13 +28,21 @@ android:layout_height="wrap_content" android:padding="10dp"> + + + app:layout_constraintTop_toBottomOf="@id/rampCaptureVersion" /> Do you want to add more data from QR? - App Version: %s + RAMPcapture Version: %s + DHIS2 App Version: %s SDK Version: %s Connected to: %s Current user: %s From f4a6262506c475327abff23bb9c5488374a39216 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 18:10:45 +0100 Subject: [PATCH 07/18] Simprints RAMP v1-adjustments README updates --- README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f2aacb4d8f..080be19de5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | Infra: signed APK releases | [simcapture-github-release-signed-apk.yml](.github/workflows/simcapture-github-release-signed-apk.yml) | New file | GitHub Action to create a GitHub Release on a merge to `main` | | Infra: signed APK releases | [app/build.gradle.kts#L85](app/build.gradle.kts#L85) | Code change | App's Package ID set to `com.simprints.simcapture` | | Infra: GitHub Actions filtering | [simcapture-disable-upstream-workflows.yml](.github/workflows/simcapture-disable-upstream-workflows.yml) | New file | GitHub Action to limit allowed Actions to the ones starting with `simcapture-` or `copilot-` | -| Docs: fork-specific README | [README.md#L1-L70](README.md#L1-L70) | Code change | This section in README | +| Docs: fork-specific README | [README.md#L1-L110](README.md#L1-L110) | Code change | This section in README | | Enrolment+ Possible Duplicates | [CustomIntentActivityResultContract.kt#L68](form/src/main/java/org/dhis2/form/ui/customintent/CustomIntentActivityResultContract.kt#L68) | Code addition | Treats non-identify Simprints callouts that return identification matches as `PossibleDuplicates` results | | Enrolment+ Possible Duplicates | [FormView.kt#L654](form/src/main/java/org/dhis2/form/ui/FormView.kt#L654) | Code addition | Stores the returned SID session and hands the enrolment form off to the possible duplicates search flow | | Enrolment+ Possible Duplicates | [EnrollmentActivity.kt#L281](app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt#L281) | Code addition | Launches possible duplicates search from the enrolment form, carrying the biometric field and returned GUID matches | @@ -71,6 +71,12 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | MFID Auto-Open Record | [SimprintsSearchViewModel.kt#L187](app/src/main/java/org/dhis2/simprints/SimprintsSearchViewModel.kt#L187) | Code addition | Consumes an eligible single MFID identification to open the matched record directly instead of showing the result list | | MFID Auto-Open Record | [SimprintsResolveSingleBiometricSearchNavigationUseCase.kt](app/src/main/java/org/dhis2/simprints/SimprintsResolveSingleBiometricSearchNavigationUseCase.kt) | New file | Resolves the biometric search to exactly one TEI before auto-opening the record | | RAMP Infra: signed APK releases | [rampcapture-github-release-signed-apk.yml](.github/workflows/rampcapture-github-release-signed-apk.yml) | New file | GitHub Action to create a RAMPcapture GitHub Release on a merge to `ramp-main` | +| RAMP Infra: version info | [rampcapture-github-release-signed-apk.yml#L90](.github/workflows/rampcapture-github-release-signed-apk.yml#L90) | Code addition | Passes the computed RAMPcapture fork number into the signed release APK build as `RAMP_CAPTURE_VERSION` | +| RAMP Infra: version info | [app/build.gradle.kts#L96](app/build.gradle.kts#L96) | Code addition | Reads `RAMP_CAPTURE_VERSION`, falls back to `local build`, and exposes it as `BuildConfig.RAMP_CAPTURE_VERSION` | +| RAMP Infra: version info | [ContextExtensions.kt#L17](app/src/main/java/org/dhis2/bindings/ContextExtensions.kt#L17) | Code addition | Formats the RAMPcapture build version for About, including commit SHA if a non-release build | +| RAMP Infra: version info | [fragment_about.xml#L31](app/src/main/res/layout/fragment_about.xml#L31) | Code addition | Adds a RAMPcapture version row above the upstream DHIS2 app and SDK version labels on the About screen | +| RAMP Infra: version info | [AboutFragment.kt#L45](app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt#L45) | Code addition | Binds the RAMPcapture version text while keeping the DHIS2 app and SDK version | +| RAMP Infra: version info | [strings.xml#L363](app/src/main/res/values/strings.xml#L363) | Code addition | Adds the `RAMPcapture Version: %s` label for the new About screen row | | RAMP ACF-1,39,40,41: datastore config | [RampDatastoreConfig.kt#L5](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L5) | New file | Defines the RAMP datastore config schema for enabled data-element history charts and program-stage history tables | | RAMP ACF-1,39,40,41: datastore config | [RampDatastoreRepository.kt#L13](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L13) | New file | Reads, validates, caches, and downloads the `simprints/ramp` DHIS2 datastore config used by RAMP features | | RAMP ACF-1,39,40,41: datastore config | [SyncPresenterImpl.kt#L272](app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt#L272) | Code addition | Syncs the RAMP datastore config after metadata download so charts and tables use current server configuration | @@ -79,16 +85,16 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | RAMP ACF-1: field history charts | [FormHistoryChartView.kt#L48](form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt#L48) | New file | Renders the configured field history chart as a line chart with visit-number X-axis labels | | RAMP ACF-1: field history charts | [FieldProvider.kt#L164](form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt#L164) | Code change | Displays the RAMP history chart directly under the matching input field | | RAMP ACF-1: field history charts | [FormViewModel.kt#L313](form/src/main/java/org/dhis2/form/ui/FormViewModel.kt#L313) | Code change | Keeps the plotted current-event value live while the user edits a field | -| RAMP ACF-39,40,41: history tables | [EventHistoryTableRepository.kt#L19](app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt#L19) | New file | Builds visit-column history tables from configured follow-up visit events, grouped by program-stage sections | +| RAMP ACF-39,40,41: history tables | [EventHistoryTableRepository.kt#L20](app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt#L20) | New file | Builds visit-column history tables from configured follow-up visit events, grouped by program-stage sections | | RAMP ACF-39,40,41: history tables | [EventHistoryTableScreen.kt#L41](app/src/main/java/org/dhis2/simprints/ramp/ui/EventHistoryTableScreen.kt#L41) | New file | Renders the history table with visit columns, date row, section headers, loading, empty, and error states | | RAMP ACF-39,40,41: history tables | [EventHistoryTableFragment.kt#L23](app/src/main/java/org/dhis2/simprints/ramp/ui/EventHistoryTableFragment.kt#L23) | New file | Shows the history table in event-capture and TEI dashboard contexts using event or enrollment arguments | | RAMP ACF-39,40,41: history tables | [EventCaptureRepositoryImpl.java#L217](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java#L217) | Code addition | Detects whether the current event's program stage has a configured RAMP history table | -| RAMP ACF-39,40,41: history tables | [EventCapturePagerAdapter.kt#L66](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt#L66) | Code change | Adds the event-capture History page and maps it to the table navigation item | +| RAMP ACF-39,40,41: history tables | [EventCapturePagerAdapter.kt#L69](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt#L69) | Code change | Adds the event-capture History page and maps it to the table navigation item | | RAMP ACF-39,40,41: history tables | [EventCapturePresenterImpl.kt#L155](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt#L155) | Code change | Adds the RAMP History navigation item to program stage (event) screens (where configured) | -| RAMP ACF-39,40,41: history tables | [EventCaptureActivity.kt#L278](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt#L278) | Code change | Expands the event-capture history table to full width in landscape and restores the split layout afterwards | +| RAMP ACF-39,40,41: history tables | [EventCaptureActivity.kt#L237](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt#L237) | Code change | Expands the event-capture history table to full width in landscape and restores the split layout afterwards | | RAMP ACF-39,40,41: history tables | [DashboardRepositoryImpl.kt#L817](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt#L817) | Code addition | Detects whether the current enrollment program has a configured RAMP history table | -| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L170](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L170) | Code addition | Adds the RAMP History navigation item to program enrollment screens (where configured) | -| RAMP ACF-39,40,41: history tables | [TeiDashboardMobileActivity.kt#L406](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt#L406) | Code addition | Opens the enrollment history table from the TEI dashboard and expands it to full width in landscape (to use space on tablets) | +| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L176](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L176) | Code addition | Adds the RAMP History navigation item to program enrollment screens (where configured) | +| RAMP ACF-39,40,41: history tables | [TeiDashboardMobileActivity.kt#L417](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt#L417) | Code addition | Opens the enrollment history table from the TEI dashboard and expands it to full width in landscape (to use space on tablets) | ### Releases From a9a479f35e3688d5f51a3f1d610e1a3adbb411d2 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 18:36:27 +0100 Subject: [PATCH 08/18] Simprints RAMP ACF-39,40,41 Table tab replaces Notes tab only if the former exists --- .../teidashboard/TeiDashboardTest.kt | 66 ++++++++++++++++++- .../teidashboard/robot/TeiDashboardRobot.kt | 8 +-- .../eventCapture/EventPageConfigurator.kt | 2 +- .../TeiDashboardPageConfigurator.kt | 2 +- .../eventCapture/EventPageConfiguratorTest.kt | 9 ++- .../TeiDashboardPageConfiguratorTest.kt | 9 ++- 6 files changed, 86 insertions(+), 10 deletions(-) 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 ebbeda6276..93c3c49e54 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -20,6 +20,7 @@ import org.dhis2.usescases.teidashboard.robot.analyticsRobot import org.dhis2.usescases.teidashboard.robot.enrollmentRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.indicatorsRobot +import org.dhis2.usescases.teidashboard.robot.noteRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Assume @@ -47,7 +48,36 @@ class TeiDashboardTest : BaseTest() { } @Test - fun shouldNotDisplayNotesTab() { + fun shouldSuccessfullyCreateANoteWhenClickCreateNote() { + enableIntents() + mockWebServerRobot.addResponse( + method = ResponseController.GET, + path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, + sdkResource = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_RESPONSE, + responseCode = 200, + ) + + setupCredentials() + + prepareTeiCompletedProgrammeAndLaunchActivity(rule) + + teiDashboardRobot(composeTestRule) { + goToNotes() + } + + noteRobot { + clickOnFabAddNewNote() + verifyNoteDetailActivityIsLaunched() + typeNote(NOTE_VALID) + clickOnSaveButton() + checkNewNoteWasCreated(NOTE_VALID) + } + + } + + @Test + fun shouldNotCreateANoteWhenClickClear() { + enableIntents() mockWebServerRobot.addResponse( method = ResponseController.GET, path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, @@ -58,7 +88,39 @@ class TeiDashboardTest : BaseTest() { prepareTeiCompletedProgrammeAndLaunchActivity(rule) teiDashboardRobot(composeTestRule) { - checkNotesTabDoesNotExist() + goToNotes() + } + + noteRobot { + clickOnFabAddNewNote() + verifyNoteDetailActivityIsLaunched() + typeNote(NOTE_INVALID) + clickOnClearButton() + clickYesOnAlertDialog() + checkNoteWasNotCreated(NOTE_INVALID) + } + } + + @Test + fun shouldOpenNotesDetailsWhenClickOnNote() { + mockWebServerRobot.addResponse( + method = ResponseController.GET, + path = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH, + sdkResource = API_UNIQUE_ID_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_RESPONSE, + responseCode = 200, + ) + + prepareTeiWithExistingNoteAndLaunchActivity(rule) + + teiDashboardRobot(composeTestRule) { + goToNotes() + } + + noteRobot { + clickOnFabAddNewNote() + typeNote(NOTE_EXISTING_TEXT) + clickOnSaveButton() + checkNoteDetails("@$USER", NOTE_EXISTING_TEXT) } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt index ee73a5e4f7..5c1ae78777 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt @@ -60,13 +60,13 @@ fun teiDashboardRobot( class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun checkNotesTabDoesNotExist() { + fun goToNotes() { composeTestRule.onNodeWithText( InstrumentationRegistry.getInstrumentation().targetContext.getString( R.string.navigation_notes - ), - useUnmergedTree = true, - ).assertDoesNotExist() + ) + ).performClick() + waitToDebounce(500) } fun goToRelationships() { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt index e1c7e03095..ab1159f2d5 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt @@ -14,7 +14,7 @@ class EventPageConfigurator( override fun displayRelationships(): Boolean = eventCaptureRepository.hasRelationships() - override fun displayNotes(): Boolean = false + override fun displayNotes(): Boolean = !displayTableView() override fun displayTableView(): Boolean = eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable() } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt index 9a383020dc..cbea64ca7d 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt @@ -12,7 +12,7 @@ class TeiDashboardPageConfigurator( override fun displayRelationships(): Boolean = dashboardRepository.programHasRelationships() - override fun displayNotes(): Boolean = false + override fun displayNotes(): Boolean = !displayTableView() override fun displayTableView(): Boolean = dashboardRepository.programHasSimprintsRampProgramStageHistoryTable() } diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt index 611addff24..9579f7f383 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt @@ -26,7 +26,14 @@ class EventPageConfiguratorTest { } @Test - fun `displayNotes should be false`() { + fun `displayNotes should be true when Simprints RAMP history table is not configured`() { + whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn false + assertTrue(pageConfigurator.displayNotes()) + } + + @Test + fun `displayNotes should be false when Simprints RAMP history table is configured`() { + whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn true assertFalse(pageConfigurator.displayNotes()) } } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt index 191c62c189..a12042d581 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt @@ -44,7 +44,14 @@ class TeiDashboardPageConfiguratorTest { } @Test - fun `Should not display the notes screen`() { + fun `Should display notes screen when history screen is not configured`() { + whenever(dashboardRepository.programHasSimprintsRampProgramStageHistoryTable()) doReturn false + assertTrue(pageConfigurator.displayNotes()) + } + + @Test + fun `Should not display notes screen when history screen is configured`() { + whenever(dashboardRepository.programHasSimprintsRampProgramStageHistoryTable()) doReturn true assertFalse(pageConfigurator.displayNotes()) } From 5f213b7d8db9593e9a72c9095305a58f2f03c508 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Jun 2026 18:49:47 +0100 Subject: [PATCH 09/18] Simprints RAMP ACF-1 optimization: avoiding reading from event repository if no chart configured for the field --- .../form/simprints/ramp/data/FormHistoryChartRepository.kt | 4 ++++ .../simprints/ramp/data/FormHistoryChartRepositoryTest.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt index 54050ba99f..d7f14a7c44 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt @@ -17,6 +17,10 @@ class FormHistoryChartRepository( fieldUiModel: FieldUiModel, configs: List, ): FormHistoryChart? { + configs.firstOrNull { config -> + config.dataElementId?.trim() == fieldUiModel.uid + } ?: return null + val currentEvent = loadCurrentEvent() ?: return null val chartConfig = configs.firstOrNull { config -> diff --git a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt index a762bc2f7f..aedc26adb6 100644 --- a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt +++ b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt @@ -154,6 +154,7 @@ class FormHistoryChartRepositoryTest { ) assertNull(chart) + verify(currentEventRepository, times(0)).blockingGet() } @Test From 2f44ff4b43bb2178fd18adb0db77a30370600d78 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 15 Jun 2026 11:43:25 +0100 Subject: [PATCH 10/18] Simprints RAMP ACF-39,40,41 table row order following section order --- .../ramp/data/EventHistoryTableRepository.kt | 9 +- .../data/EventHistoryTableRepositoryTest.kt | 131 +++++++++++++++--- 2 files changed, 113 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt b/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt index c3187041f6..eea9847bb7 100644 --- a/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt +++ b/app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt @@ -209,14 +209,15 @@ class EventHistoryTableRepository( rows = followUpRows, ) } else { + val rowsByDataElementUid = followUpRows.associateBy { row -> row.dataElementUid } + sections.mapNotNull { section -> - val sectionDataElementUids = + val rows = section .dataElements() .orEmpty() - .mapNotNull { dataElement -> dataElement.uid() } - .toSet() - val rows = followUpRows.filter { row -> row.dataElementUid in sectionDataElementUids } + .mapNotNull { dataElement -> dataElement.uid()?.let(rowsByDataElementUid::get) } + .distinctBy { row -> row.dataElementUid } rows .takeIf { it.isNotEmpty() } diff --git a/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt b/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt index 52710d4eca..37e443480a 100644 --- a/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt +++ b/app/src/test/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepositoryTest.kt @@ -262,6 +262,64 @@ class EventHistoryTableRepositoryTest { ) } + @Test + fun `table should follow section form order for sections and rows`() { + stubRampConfig() + stubRowsAndSections( + programStageDataElementUids = + listOf( + VISIT_NUMBER_UID, + WEIGHT_UID, + STATUS_UID, + BOOLEAN_UID, + EXCLUDED_UID, + ), + sections = + listOf( + TestSectionDefinition( + uid = "second-section", + displayName = "Second section", + sortOrder = 2, + dataElementUids = listOf(BOOLEAN_UID), + ), + TestSectionDefinition( + uid = "first-section", + displayName = "First section", + sortOrder = 1, + dataElementUids = listOf(STATUS_UID, WEIGHT_UID), + ), + ), + ) + stubFollowUpEvents(emptyList()) + stubOptions() + + val table = + EventHistoryTableRepository( + d2 = d2, + simprintsRampDatastoreRepository = simprintsRampDatastoreRepository, + programUid = PROGRAM_UID, + enrollmentUid = ENROLLMENT_UID, + ).getTable() + + assertEquals(listOf("First section", "Second section"), table?.sections?.map { it.title }) + assertEquals( + listOf("Status", "Weight"), + table + ?.sections + ?.first() + ?.rows + ?.map { it.label }, + ) + assertEquals( + listOf("Confirmed"), + table + ?.sections + ?.get(1) + ?.rows + ?.map { it.label }, + ) + } + private fun stubRampConfig() { whenever(simprintsRampDatastoreRepository.getConfig()) doReturn RampDatastoreConfig( @@ -303,21 +361,49 @@ class EventHistoryTableRepositoryTest { whenever(activeFollowUpEvents.blockingGet()) doReturn events } - private fun stubRowsAndSections() { + private fun stubRowsAndSections( + programStageDataElementUids: List = + listOf( + VISIT_NUMBER_UID, + WEIGHT_UID, + STATUS_UID, + BOOLEAN_UID, + EXCLUDED_UID, + ), + sections: List = + listOf( + TestSectionDefinition( + uid = "section", + displayName = "Follow up", + sortOrder = 1, + dataElementUids = listOf(WEIGHT_UID, STATUS_UID, BOOLEAN_UID, EXCLUDED_UID), + ), + TestSectionDefinition( + uid = "empty-section", + displayName = "Hidden", + sortOrder = 2, + dataElementUids = listOf(EXCLUDED_UID), + ), + ), + ) { val programStage = ObjectWithUid.create(PROGRAM_STAGE_UID) val weightDataElement = dataElement(WEIGHT_UID) val statusDataElement = dataElement(STATUS_UID) val booleanDataElement = dataElement(BOOLEAN_UID) val excludedDataElement = dataElement(EXCLUDED_UID) val visitNumberDataElement = dataElement(VISIT_NUMBER_UID) - val programStageDataElementsForStage = + val dataElementsByUid = listOf( - programStageDataElement(programStage, visitNumberDataElement), - programStageDataElement(programStage, weightDataElement), - programStageDataElement(programStage, statusDataElement), - programStageDataElement(programStage, booleanDataElement), - programStageDataElement(programStage, excludedDataElement), - ) + weightDataElement, + statusDataElement, + booleanDataElement, + excludedDataElement, + visitNumberDataElement, + ).associateBy { dataElement -> dataElement.uid().orEmpty() } + val programStageDataElementsForStage = + programStageDataElementUids.map { dataElementUid -> + programStageDataElement(programStage, dataElementsByUid.getValue(dataElementUid)) + } whenever(d2.programModule().programStageDataElements()) doReturn programStageDataElements whenever(programStageDataElements.withRenderType()) doReturn programStageDataElementsWithRenderType @@ -332,24 +418,16 @@ class EventHistoryTableRepositoryTest { whenever(programStageSectionFilter.eq(PROGRAM_STAGE_UID)) doReturn programStageSectionsByStage whenever(programStageSectionsByStage.withDataElements()) doReturn programStageSectionsWithDataElements whenever(programStageSectionsWithDataElements.blockingGet()) doReturn - listOf( + sections.map { section -> ProgramStageSection .builder() - .uid("section") - .displayName("Follow up") - .sortOrder(1) + .uid(section.uid) + .displayName(section.displayName) + .sortOrder(section.sortOrder) .programStage(programStage) - .dataElements(listOf(weightDataElement, statusDataElement, booleanDataElement, excludedDataElement)) - .build(), - ProgramStageSection - .builder() - .uid("empty-section") - .displayName("Hidden") - .sortOrder(2) - .programStage(programStage) - .dataElements(listOf(excludedDataElement)) - .build(), - ) + .dataElements(section.dataElementUids.map(dataElementsByUid::getValue)) + .build() + } whenever( d2 .dataElementModule() @@ -465,6 +543,13 @@ class EventHistoryTableRepositoryTest { private fun dataElement(uid: String): DataElement = DataElement.builder().uid(uid).build() + private data class TestSectionDefinition( + val uid: String, + val displayName: String, + val sortOrder: Int?, + val dataElementUids: List, + ) + private companion object { const val CURRENT_EVENT_UID = "current" const val ENROLLMENT_UID = "enrollment" From 0e20aca7845e285d24dd083bc49b7c0c971419c7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 15 Jun 2026 11:45:01 +0100 Subject: [PATCH 11/18] Simprints RAMP ACF-39,40,41 tables renamed from History to Chart --- .../eventCapture/EventCapturePresenterImpl.kt | 2 +- .../java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt index 81a1ce393a..7c97b28603 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt @@ -158,7 +158,7 @@ class EventCapturePresenterImpl( id = NavigationPage.TABLE_VIEW, icon = Icons.Outlined.TableChart, selectedIcon = Icons.Filled.TableChart, - label = resourceManager.getString(R.string.navigation_simprints_ramp_history), + label = resourceManager.getString(R.string.navigation_simprints_ramp_history_chart), ), ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index 9e1a451acb..112c898a97 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -179,7 +179,7 @@ class DashboardViewModel( id = TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, icon = Icons.Outlined.TableChart, selectedIcon = Icons.Filled.TableChart, - label = resourcesManager.getString(R.string.navigation_simprints_ramp_history), + label = resourcesManager.getString(R.string.navigation_simprints_ramp_history_chart), ), ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0cb587fa48..e232d4731b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -760,7 +760,7 @@ Charts Relations Notes - History + Chart Data entry Form Details From 19963a47cbc7c7e9c55cf8f4a9a6b260ac71c720 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 15 Jun 2026 12:15:50 +0100 Subject: [PATCH 12/18] Simprints RAMP ACF-1 graphs have decimal places limited in datastore config --- .../ramp/model/RampDatastoreConfig.kt | 2 ++ .../repository/RampDatastoreRepositoryTest.kt | 4 ++- .../ramp/data/FormHistoryChartRepository.kt | 1 + .../simprints/ramp/model/FormHistoryChart.kt | 1 + .../simprints/ramp/ui/FormHistoryChartView.kt | 20 ++++++++++---- .../data/FormHistoryChartRepositoryTest.kt | 2 ++ .../ramp/ui/IntegerAwareValueFormatterTest.kt | 27 +++++++++++++++++++ 7 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 form/src/test/java/org/dhis2/form/simprints/ramp/ui/IntegerAwareValueFormatterTest.kt diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt index a1aa4af258..399a6030c0 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt @@ -22,6 +22,8 @@ data class DataElementHistoryChartConfig( val xAxisVisitNumberDataElementId: String? = null, @SerializedName("followUpVisitMaxNumber") val followUpVisitMaxNumber: Int? = null, + @SerializedName("displayMaxDecimalPlaces") + val displayMaxDecimalPlaces: Int? = null, ) { fun isValid(): Boolean = !programId.isNullOrBlank() && diff --git a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt index 62196b1f40..aeb92a9f30 100644 --- a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt +++ b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt @@ -30,7 +30,8 @@ class RampDatastoreRepositoryTest { "followUpVisitProgramStageId": "follow-stage", "dataElementId": "weight", "xAxisVisitNumberDataElementId": "visit-number", - "followUpVisitMaxNumber": 12 + "followUpVisitMaxNumber": 12, + "displayMaxDecimalPlaces": 1 }, { "programId": "program", @@ -69,6 +70,7 @@ class RampDatastoreRepositoryTest { assertEquals("weight", chart.dataElementId) assertEquals("visit-number", chart.xAxisVisitNumberDataElementId) assertEquals(12, chart.followUpVisitMaxNumber) + assertEquals(1, chart.displayMaxDecimalPlaces) } assertEquals(1, config.programStageHistoryTables.size) config.programStageHistoryTables.first().let { table -> diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt index d7f14a7c44..962ec49ea6 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt @@ -72,6 +72,7 @@ class FormHistoryChartRepository( labels = labels, values = values, currentValueIndex = currentValueIndex, + displayMaxDecimalPlaces = chartConfig.displayMaxDecimalPlaces, ) } diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/model/FormHistoryChart.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/model/FormHistoryChart.kt index 55e96d3449..e8759541ac 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/model/FormHistoryChart.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/model/FormHistoryChart.kt @@ -5,6 +5,7 @@ data class FormHistoryChart( val labels: List, val values: List, val currentValueIndex: Int? = null, + val displayMaxDecimalPlaces: Int? = null, ) { fun withCurrentValue(value: String?): FormHistoryChart { val index = currentValueIndex?.takeIf { it in values.indices } ?: return this diff --git a/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt b/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt index 6155396a12..823bf75752 100644 --- a/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt +++ b/form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt @@ -43,6 +43,7 @@ private const val HISTORY_CHART_Y_AXIS_LABEL_COUNT = 5 private const val HISTORY_CHART_FLAT_RANGE_EPSILON = 0.0001f private const val HISTORY_CHART_INTEGER_GRANULARITY_MIN_SPAN = 1f private const val HISTORY_CHART_INTEGER_GRANULARITY = 1f +private const val HISTORY_CHART_DEFAULT_MAX_DECIMAL_PLACES = 2 @Composable fun FormHistoryChartView(historyChart: FormHistoryChart) { @@ -87,11 +88,12 @@ private fun LineChart.configureHistoryChart( historyChart: FormHistoryChart, graph: Graph, ) { - val valueFormatter = IntegerAwareValueFormatter() + val axisValueFormatter = IntegerAwareValueFormatter() + val pointValueFormatter = IntegerAwareValueFormatter(historyChart.displayMaxDecimalPlaces) setHighlightPerTapEnabled(false) setHighlightPerDragEnabled(false) - data = graph.toHistoryLineData(valueFormatter) + data = graph.toHistoryLineData(pointValueFormatter) xAxis.apply { axisMinimum = -1f axisMaximum = historyChart.labels.size.toFloat() @@ -102,7 +104,7 @@ private fun LineChart.configureHistoryChart( textSize = HISTORY_CHART_TEXT_SIZE } axisLeft.apply { - this.valueFormatter = valueFormatter + this.valueFormatter = axisValueFormatter setLabelCount(HISTORY_CHART_Y_AXIS_LABEL_COUNT, false) textSize = HISTORY_CHART_TEXT_SIZE } @@ -180,9 +182,14 @@ private fun Graph.toHistoryLineData(valueFormatter: ValueFormatter) = setValueTextSize(HISTORY_CHART_TEXT_SIZE) } -private class IntegerAwareValueFormatter : ValueFormatter() { +internal class IntegerAwareValueFormatter( + maxDecimalPlaces: Int? = null, +) : ValueFormatter() { private val decimalFormat = - DecimalFormat("0.##", DecimalFormatSymbols(Locale.US)) + DecimalFormat("0", DecimalFormatSymbols(Locale.US)).apply { + minimumFractionDigits = 0 + maximumFractionDigits = maxDecimalPlaces.validMaxDecimalPlaces() + } override fun getFormattedValue(value: Float): String = if (abs(value - value.roundToInt()) < INTEGER_VALUE_EPSILON) { @@ -201,6 +208,9 @@ private class IntegerAwareValueFormatter : ValueFormatter() { } } +private fun Int?.validMaxDecimalPlaces(): Int = + this?.takeIf { it >= 0 } ?: HISTORY_CHART_DEFAULT_MAX_DECIMAL_PLACES + private fun FormHistoryChart.toAnalyticsGraph(): Graph = Graph( title = title, diff --git a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt index aedc26adb6..fe562a3f57 100644 --- a/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt +++ b/form/src/test/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepositoryTest.kt @@ -89,6 +89,7 @@ class FormHistoryChartRepositoryTest { assertEquals(listOf("0", "1", "2", "3"), chart?.labels) assertEquals(listOf(8f, 9.5f, null, null), chart?.values) assertEquals(1, chart?.currentValueIndex) + assertEquals(1, chart?.displayMaxDecimalPlaces) } @Test @@ -283,6 +284,7 @@ class FormHistoryChartRepositoryTest { dataElementId = DATA_ELEMENT_UID, xAxisVisitNumberDataElementId = VISIT_NUMBER_UID, followUpVisitMaxNumber = 3, + displayMaxDecimalPlaces = 1, ) private fun stubCurrentEvent(event: Event) { diff --git a/form/src/test/java/org/dhis2/form/simprints/ramp/ui/IntegerAwareValueFormatterTest.kt b/form/src/test/java/org/dhis2/form/simprints/ramp/ui/IntegerAwareValueFormatterTest.kt new file mode 100644 index 0000000000..55ef40cb33 --- /dev/null +++ b/form/src/test/java/org/dhis2/form/simprints/ramp/ui/IntegerAwareValueFormatterTest.kt @@ -0,0 +1,27 @@ +package org.dhis2.form.simprints.ramp.ui + +import org.junit.Assert.assertEquals +import org.junit.Test + +class IntegerAwareValueFormatterTest { + @Test + fun `getFormattedValue should use configured maximum decimal places`() { + val formatter = IntegerAwareValueFormatter(maxDecimalPlaces = 1) + + assertEquals("9.6", formatter.getFormattedValue(9.56f)) + } + + @Test + fun `getFormattedValue should use two decimal places by default`() { + val formatter = IntegerAwareValueFormatter() + + assertEquals("9.57", formatter.getFormattedValue(9.567f)) + } + + @Test + fun `getFormattedValue should not add decimals to integers`() { + val formatter = IntegerAwareValueFormatter(maxDecimalPlaces = 1) + + assertEquals("9", formatter.getFormattedValue(9f)) + } +} From 02d4766d6c3e58b97b950ef7fc5705d69a7287d9 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Jun 2026 13:22:27 +0100 Subject: [PATCH 13/18] Simprints RAMP: search disabled in some programs (driven by datastore configuration) --- .../searchTrackEntity/SearchJavaToCompose.kt | 3 +- .../searchTrackEntity/SearchRepository.java | 2 + .../SearchRepositoryImpl.java | 9 ++++ .../searchTrackEntity/SearchTEActivity.kt | 29 +++++++++++- .../searchTrackEntity/SearchTEIViewModel.kt | 42 +++++++++++++++-- .../searchTrackEntity/SearchTEScreenState.kt | 1 + .../listView/SearchTEList.kt | 2 + .../searchTrackEntity/mapView/SearchTEMap.kt | 26 ++++++----- .../ui/SearchScreenConfigurator.kt | 45 ++++++++++++++----- .../searchTrackEntity/ui/SearchTEUi.kt | 34 +++++++++----- .../SearchTEIViewModelTest.kt | 42 +++++++++++++++++ .../ramp/model/RampDatastoreConfig.kt | 13 +++++- .../repository/RampDatastoreRepository.kt | 22 +++++++++ .../repository/RampDatastoreRepositoryTest.kt | 29 ++++++++++++ 14 files changed, 259 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt index ffc216171b..df6669b71a 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt @@ -19,6 +19,7 @@ fun ComposeView?.setLandscapeOpenSearchButton( DHIS2Theme { val screenState by searchTEIViewModel.screenState.observeAsState() val teTypeName by searchTEIViewModel.teTypeName.observeAsState() + val isSearchEnabled by searchTEIViewModel.isSearchEnabled.observeAsState(true) val visible = screenState?.let { @@ -26,7 +27,7 @@ fun ComposeView?.setLandscapeOpenSearchButton( } ?: false val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - AnimatedVisibility(visible = isLandscape && visible && !teTypeName.isNullOrBlank()) { + AnimatedVisibility(visible = isLandscape && visible && isSearchEnabled && !teTypeName.isNullOrBlank()) { WrappedSearchButton(onClick = onClick, teTypeName = teTypeName!!) } } 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 0c199898d1..db2e4dbf7e 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -79,6 +79,8 @@ public interface SearchRepository { boolean canCreateInProgramWithoutSearch(); + boolean isSearchEnabled(); + void setCurrentTheme(@org.jetbrains.annotations.Nullable ProgramSpinnerModel selectedProgram); List trackedEntityTypeFields(); 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 cd66d34123..dd8b1c8e9d 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -3,6 +3,7 @@ import android.database.sqlite.SQLiteConstraintException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.gson.Gson; import org.dhis2.R; import org.dhis2.bindings.ExtensionsKt; import org.dhis2.bindings.ValueExtensionsKt; @@ -18,6 +19,7 @@ import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository; import org.dhis2.data.dhislogic.DhisEnrollmentUtils; import org.dhis2.data.forms.dataentry.SearchTEIRepository; import org.dhis2.data.forms.dataentry.ValueStore; @@ -124,6 +126,7 @@ public class SearchRepositoryImpl implements SearchRepository { private final MetadataIconProvider metadataIconProvider; private final ProfilePictureProvider profilePictureProvider; + private final RampDatastoreRepository rampDatastoreRepository; private CustomIntentRepository customIntentRepository; SearchRepositoryImpl(String teiType, @@ -165,6 +168,7 @@ public class SearchRepositoryImpl implements SearchRepository { this.metadataIconProvider = metadataIconProvider; this.profilePictureProvider = profilePictureProvider; this.customIntentRepository = customIntentRepository; + this.rampDatastoreRepository = new RampDatastoreRepository(d2, new Gson()); } @@ -1012,6 +1016,11 @@ public boolean canCreateInProgramWithoutSearch() { } } + @Override + public boolean isSearchEnabled() { + return rampDatastoreRepository.isSearchEnabled(currentProgram); + } + private boolean displayOrgUnit() { return d2.organisationUnitModule().organisationUnits() .byProgramUids(Collections.singletonList(currentProgram)) diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt index d80600ff4f..01bc7e7aa0 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View +import android.view.ViewTreeObserver import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.animation.AnimatedVisibility @@ -154,6 +155,7 @@ class SearchTEActivity : } private var currentContent: Content? = null + private var initialLandscapeDrawReady = true @OptIn(ExperimentalAnimationApi::class) @SuppressLint("ClickableViewAccessibility") @@ -167,6 +169,7 @@ class SearchTEActivity : super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_search) + postponeInitialLandscapeDraw() // prevents jitter depending on if search panel set to visible or not val currentScreen = savedInstanceState?.getString(CURRENT_SCREEN).orEmpty() if (currentScreen.isNotBlank()) { currentContent = Content.valueOf(currentScreen) @@ -188,7 +191,7 @@ class SearchTEActivity : if (isLandscape()) { viewModel.filtersOpened.observe(this) { isOpened: Boolean -> - if (java.lang.Boolean.TRUE == isOpened) { + if (java.lang.Boolean.TRUE == isOpened || viewModel.isSearchEnabled.value == false) { binding.mainComponent.clipWithRoundedCorners(16.dp) } else { binding.mainComponent.clipWithTopRightRoundedCorner(16.dp) @@ -541,10 +544,32 @@ class SearchTEActivity : } private fun observeScreenState() { - viewModel.screenState.observe(this, searchScreenConfigurator::configure) + viewModel.screenState.observe(this) { screenState -> + searchScreenConfigurator.configure(screenState) + initialLandscapeDrawReady = true + binding.root.invalidate() + } viewModel.screenState.observe(this, viewModel::updateBackdrop) } + private fun postponeInitialLandscapeDraw() { + if (!isLandscape()) return + initialLandscapeDrawReady = false + binding.root.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean = + if (initialLandscapeDrawReady) { + binding.root.viewTreeObserver + .takeIf { it.isAlive } + ?.removeOnPreDrawListener(this) + true + } else { + false + } + }, + ) + } + private fun observeDownload() { viewModel.downloadResult.observe(this) { result: TeiDownloadResult -> result.handleResult( 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 74bc1e7a49..d8fbd38b54 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -148,6 +148,9 @@ class SearchTEIViewModel( private val _screenState = MutableLiveData() val screenState: LiveData = _screenState + private val _isSearchEnabled = MutableLiveData(true) + val isSearchEnabled: LiveData = _isSearchEnabled + val createButtonScrollVisibility = MutableLiveData(false) val isScrollingDown = MutableLiveData(false) val simprintsBiometricSearchNavigation: Flow = @@ -214,6 +217,11 @@ class SearchTEIViewModel( init { viewModelScope.launch(dispatchers.io()) { + val isSearchEnabled = searchRepository.isSearchEnabled() + withContext(dispatchers.ui()) { + _isSearchEnabled.value = isSearchEnabled + updateSearchEnabledInCurrentScreen(isSearchEnabled) + } createButtonScrollVisibility.postValue( searchRepository.canCreateInProgramWithoutSearch(), ) @@ -294,10 +302,12 @@ class SearchTEIViewModel( _screenState.value.takeIf { it?.screenState == SearchScreenState.MAP }?.let { searching = (it as SearchList).isSearching || isSimprintsBiometricNoMatchesSearch() } + val isSearchEnabled = isSearchEnabled() val displayFrontPageList = searchRepository.getProgram(initialProgramUid)?.displayFrontPageList() ?: true val shouldForceSearch = - !displayFrontPageList && + isSearchEnabled && + !displayFrontPageList && !searchRepository.canCreateInProgramWithoutSearch() && !searching && filtersActive.value == false @@ -326,6 +336,7 @@ class SearchTEIViewModel( .getProgram(initialProgramUid) ?.minAttributesRequiredToSearch() ?: 1, + isEnabled = isSearchEnabled, isForced = shouldForceSearch, isOpened = shouldForceSearch, ), @@ -363,6 +374,7 @@ class SearchTEIViewModel( .getProgram(initialProgramUid) ?.minAttributesRequiredToSearch() ?: 1, + isEnabled = isSearchEnabled(), isForced = false, isOpened = false, ), @@ -384,6 +396,8 @@ class SearchTEIViewModel( } fun setSearchScreen() { + if (!isSearchEnabled()) return + _screenState.postValue( SearchList( previousSate = _screenState.value?.screenState ?: SearchScreenState.NONE, @@ -416,6 +430,8 @@ class SearchTEIViewModel( } fun onSearchFormRequested() { + if (!isSearchEnabled()) return + if (shouldLaunchSimprintsBiometricIdentification()) { simprintsSearchViewModel.clearPendingSession() viewModelScope.launch { @@ -1172,8 +1188,10 @@ class SearchTEIViewModel( private fun handleInitWithoutData() { val result = - when (searchRepository.canCreateInProgramWithoutSearch()) { - true -> + when { + !isSearchEnabled() -> emptyList() + + searchRepository.canCreateInProgramWithoutSearch() -> listOf( SearchResult( SearchResult.SearchResultType.SEARCH_OR_CREATE, @@ -1181,7 +1199,7 @@ class SearchTEIViewModel( ), ) - false -> + else -> listOf( SearchResult( SearchResult.SearchResultType.SEARCH, @@ -1192,6 +1210,22 @@ class SearchTEIViewModel( _dataResult.postValue(result) } + private fun isSearchEnabled(): Boolean = _isSearchEnabled.value ?: true + + private fun updateSearchEnabledInCurrentScreen(isEnabled: Boolean) { + (_screenState.value as? SearchList)?.let { currentScreen -> + _screenState.value = + currentScreen.copy( + searchForm = + currentScreen.searchForm.copy( + isEnabled = isEnabled, + isForced = currentScreen.searchForm.isForced && isEnabled, + isOpened = currentScreen.searchForm.isOpened && isEnabled, + ), + ) + } + } + fun onBackPressed( isPortrait: Boolean, searchOrFilterIsOpen: Boolean, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEScreenState.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEScreenState.kt index c44d643c9a..9e350440df 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEScreenState.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEScreenState.kt @@ -20,6 +20,7 @@ data class SearchList( data class SearchForm( val queryHasData: Boolean, val minAttributesToSearch: Int, + val isEnabled: Boolean = true, val isForced: Boolean = false, val isOpened: Boolean = false, ) 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 e2b2021cfd..b3a800e259 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 @@ -223,6 +223,7 @@ class SearchTEList : FragmentGlobalAbstract() { if (!teTypeName.isNullOrBlank()) { val isFilterOpened by viewModel.filtersOpened.observeAsState(false) + val isSearchEnabled by viewModel.isSearchEnabled.observeAsState(true) val createButtonVisibility by viewModel .createButtonScrollVisibility .observeAsState(true) @@ -240,6 +241,7 @@ class SearchTEList : FragmentGlobalAbstract() { FullSearchButtonAndWorkingList( teTypeName = teTypeName!!, modifier = Modifier, + searchButtonVisible = isSearchEnabled, createButtonVisible = createButtonVisibility, closeFilterVisibility = isFilterOpened, isLandscape = isLandscape(), diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt index 541bcb6687..47e9380724 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -141,6 +142,7 @@ class SearchTEMap : FragmentGlobalAbstract() { val locationState = teiMapManager?.locationState?.collectAsState() val mapDataFinishedLoading = teiMapManager?.dataFinishedLoading?.collectAsState() + val isSearchEnabled by viewModel.isSearchEnabled.observeAsState(true) LaunchedEffect(key1 = clickedItem) { clickedItem?.let { @@ -184,17 +186,19 @@ class SearchTEMap : FragmentGlobalAbstract() { } }, actionButtons = { - IconButton( - style = IconButtonStyle.TONAL, - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_search), - contentDescription = "", - tint = TextColor.OnPrimaryContainer, - ) - }, - ) { - viewModel.onSearchFormRequested() + if (isSearchEnabled) { + IconButton( + style = IconButtonStyle.TONAL, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "", + tint = TextColor.OnPrimaryContainer, + ) + }, + ) { + viewModel.onSearchFormRequested() + } } mapDataFinishedLoading?.let { if (it.value) { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt index 82e7483cad..c27348378f 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt @@ -18,17 +18,23 @@ class SearchScreenConfigurator( val binding: ActivitySearchBinding, val filterIsOpenCallback: (isOpen: Boolean) -> Unit, ) { + private var hasConfiguredLandscape = false + fun configure(screenState: SearchTEScreenState) { when (screenState) { - is SearchAnalytics -> configureLandscapeAnalyticsScreen(true) + is SearchAnalytics -> configureLandscapeAnalyticsScreen(true, withAnimation = hasConfiguredLandscape) is SearchList -> if (isPortrait()) { configureListScreen(screenState) } else { - if (screenState.screenState != SearchScreenState.MAP) { - configureLandscapeAnalyticsScreen(false) + val withAnimation = hasConfiguredLandscape + if (screenState.screenState != SearchScreenState.MAP && + (screenState.searchForm.isEnabled || screenState.searchFilters.isOpened) + ) { + configureLandscapeAnalyticsScreen(false, withAnimation = withAnimation) } - configureLandscapeListScreen(screenState) + configureLandscapeListScreen(screenState, withAnimation) + hasConfiguredLandscape = true } } } @@ -45,22 +51,39 @@ class SearchScreenConfigurator( setFiltersVisibility(!searchConfiguration.searchForm.isOpened) } - private fun configureLandscapeListScreen(searchConfiguration: SearchList) { - if (searchConfiguration.searchFilters.isOpened) { - openFilters() - } else { - openSearch() + private fun configureLandscapeListScreen(searchConfiguration: SearchList, withAnimation: Boolean) { + when { + searchConfiguration.searchFilters.isOpened -> { + if (searchConfiguration.screenState == SearchScreenState.MAP) { + configureLandscapeAnalyticsScreen(false, withAnimation = withAnimation) + } + openFilters() + } + + searchConfiguration.searchForm.isEnabled -> { + if (searchConfiguration.screenState == SearchScreenState.MAP) { + configureLandscapeAnalyticsScreen(false, withAnimation = withAnimation) + } + openSearch() + } + + else -> { + configureLandscapeAnalyticsScreen(true, withAnimation = false) + closeBackdrop() + } } syncButtonVisibility(true) setFiltersVisibility(true) } - private fun configureLandscapeAnalyticsScreen(expanded: Boolean) { + private fun configureLandscapeAnalyticsScreen(expanded: Boolean, withAnimation: Boolean = true) { val constraintSet = ConstraintSet() constraintSet.clone(binding.backdropLayout) constraintSet.setGuidelinePercent(R.id.backdropGuideDiv, if (expanded) 0.0f else 0.26f) - TransitionManager.beginDelayedTransition(binding.backdropLayout) + if (withAnimation) { + TransitionManager.beginDelayedTransition(binding.backdropLayout) + } constraintSet.applyTo(binding.backdropLayout) } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt index 56eda9bcf9..1ab9540218 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt @@ -244,6 +244,7 @@ fun WrappedSearchButton( fun FullSearchButtonAndWorkingList( teTypeName: String, modifier: Modifier, + searchButtonVisible: Boolean = true, createButtonVisible: Boolean = false, closeFilterVisibility: Boolean = false, isLandscape: Boolean = false, @@ -258,7 +259,12 @@ fun FullSearchButtonAndWorkingList( workingListViewModel: WorkingListViewModel? = null, ) { Column(modifier = modifier) { - if (!isLandscape || queryData.isNotEmpty()) { + val displaySearchControls = + searchButtonVisible || + createButtonVisible || + closeFilterVisibility + + if (displaySearchControls && (!isLandscape || queryData.isNotEmpty())) { Row( modifier = Modifier.padding( @@ -276,7 +282,7 @@ fun FullSearchButtonAndWorkingList( .weight(1f), verticalArrangement = Arrangement.spacedBy(Spacing.Spacing8), ) { - if (queryData.isNotEmpty()) { + if (queryData.isNotEmpty() && searchButtonVisible) { SearchButtonWithQuery( modifier = Modifier.fillMaxWidth(), queryData = queryData, @@ -285,10 +291,11 @@ fun FullSearchButtonAndWorkingList( onClick = onSearchClick, onClearSearchQuery = onClearSearchQuery, ) - } else { + } else if (searchButtonVisible || createButtonVisible) { SearchAndCreateTEIButton( onSearchClick = onSearchClick, teTypeName = teTypeName, + searchButtonVisible = searchButtonVisible, createButtonVisible = createButtonVisible, onEnrollClick = onEnrollClick, ) @@ -322,7 +329,11 @@ fun FullSearchButtonAndWorkingList( } } - if (isSimprintsBiometricSearch && !isSimprintsPossibleDuplicatesSearch && queryData.isNotEmpty()) { + if (searchButtonVisible && + isSimprintsBiometricSearch && + !isSimprintsPossibleDuplicatesSearch && + queryData.isNotEmpty() + ) { SimprintsBiometricSearchFallbackButton( modifier = Modifier.padding( @@ -347,15 +358,18 @@ fun FullSearchButtonAndWorkingList( private fun SearchAndCreateTEIButton( onSearchClick: () -> Unit, teTypeName: String, + searchButtonVisible: Boolean, createButtonVisible: Boolean, onEnrollClick: () -> Unit, ) { - SearchButton( - modifier = Modifier.fillMaxWidth(), - onClick = onSearchClick, - teTypeName = teTypeName, - createButtonVisible = createButtonVisible, - ) + if (searchButtonVisible) { + SearchButton( + modifier = Modifier.fillMaxWidth(), + onClick = onSearchClick, + teTypeName = teTypeName, + createButtonVisible = createButtonVisible, + ) + } if (createButtonVisible) { AddNewButton( 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 cfcce36665..22448d5490 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModelTest.kt @@ -106,6 +106,7 @@ class SearchTEIViewModelTest { whenever(pageConfigurator.initVariables()) doReturn pageConfigurator setCurrentProgram(testingProgram()) whenever(repository.canCreateInProgramWithoutSearch()) doReturn true + whenever(repository.isSearchEnabled()) doReturn true whenever(repository.getTrackedEntityType()) doReturn testingTrackedEntityType() whenever(repository.filtersApplyOnGlobalSearch()) doReturn true whenever(filterManager.stateFilters) doReturn emptyList() @@ -209,6 +210,47 @@ class SearchTEIViewModelTest { assertTrue(screenState is SearchList) } + @Test + fun `Should not open search screen when search is disabled`() { + whenever(repository.isSearchEnabled()) doReturn false + viewModel = + SearchTEIViewModel( + initialProgram, + initialQuery, + repository, + repositoryKt, + pageConfigurator, + mapDataRepository, + networkUtils, + object : DispatcherProvider { + override fun io(): CoroutineDispatcher = testingDispatcher + + override fun computation(): CoroutineDispatcher = testingDispatcher + + override fun ui(): CoroutineDispatcher = testingDispatcher + }, + mapStyleConfiguration, + resourceManager = resourceManager, + displayNameProvider = displayNameProvider, + filterManager = filterManager, + simprintsSearchViewModel = simprintsSearchViewModel, + loadSimprintsBiometricSearchResultsUseCase = loadSimprintsBiometricSearchResultsUseCase, + mapSimprintsBiometricSearchResult = mapSimprintsBiometricSearchResult, + ) + testingDispatcher.scheduler.advanceUntilIdle() + + viewModel.setListScreen() + viewModel.setSearchScreen() + viewModel.onSearchFormRequested() + + val screenState = viewModel.screenState.value + assertFalse(viewModel.isSearchEnabled.value!!) + assertTrue(screenState is SearchList) + val searchForm = (screenState as SearchList).searchForm + assertFalse(searchForm.isEnabled) + assertFalse(searchForm.isOpened) + } + @Test fun `Should request Simprints biometric identification launch instead of opening search form`() = runTest { diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt index 399a6030c0..89e3b7f9f3 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt @@ -5,10 +5,12 @@ import com.google.gson.annotations.SerializedName data class RampDatastoreConfig( val dataElementHistoryCharts: List = emptyList(), val programStageHistoryTables: List = emptyList(), + val programSpecificSettings: List = emptyList(), ) { fun isNotEmpty(): Boolean = dataElementHistoryCharts.isNotEmpty() || - programStageHistoryTables.isNotEmpty() + programStageHistoryTables.isNotEmpty() || + programSpecificSettings.isNotEmpty() } data class DataElementHistoryChartConfig( @@ -51,3 +53,12 @@ data class ProgramStageHistoryTableConfig( !headerVisitNumberDataElementId.isNullOrBlank() && (followUpVisitMaxNumber ?: -1) >= 0 } + +data class ProgramSpecificSetting( + @SerializedName("programId") + val programId: String? = null, + @SerializedName("isSearchEnabled") + val isSearchEnabled: Boolean? = null, +) { + fun isValid(): Boolean = !programId.isNullOrBlank() +} diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt index 83b35415c2..944ee528f1 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt @@ -6,6 +6,7 @@ import com.google.gson.JsonParseException import com.google.gson.JsonParser import org.dhis2.commons.simprints.ramp.model.DataElementHistoryChartConfig import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig +import org.dhis2.commons.simprints.ramp.model.ProgramSpecificSetting import org.dhis2.commons.simprints.ramp.model.RampDatastoreConfig import org.hisp.dhis.android.core.D2 import timber.log.Timber @@ -37,6 +38,20 @@ class RampDatastoreRepository( .blockingDownload() } + fun isSearchEnabled(programId: String?): Boolean { + if (programId.isNullOrBlank()) return true + + return try { + getConfig() + .programSpecificSettings + .firstOrNull { it.programId == programId } + ?.isSearchEnabled != false + } catch (exception: RuntimeException) { + Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) + true + } + } + private fun getLocalRampDatastoreValue(): String? = d2 .dataStoreModule() @@ -62,6 +77,12 @@ class RampDatastoreRepository( ?.parseList() .orEmpty() .filter { it.isValid() }, + programSpecificSettings = + root + .get(PROGRAM_SPECIFIC_SETTINGS_KEY) + ?.parseList() + .orEmpty() + .filter { it.isValid() }, ) } catch (exception: JsonParseException) { Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) @@ -133,6 +154,7 @@ class RampDatastoreRepository( private const val RAMP_DATASTORE_KEY = "ramp" private const val DATA_ELEMENT_HISTORY_CHARTS_KEY = "dataElementHistoryCharts" private const val PROGRAM_STAGE_HISTORY_TABLE_KEY = "programStageHistoryTable" + private const val PROGRAM_SPECIFIC_SETTINGS_KEY = "programSpecificSettings" private const val DATASTORE_JSON_WRAPPER_NAME = "JsonWrapper" private const val DATASTORE_JSON_WRAPPER_JSON_FIELD = "json" private const val RAMP_DATASTORE_PARSE_ERROR = "Failed to parse Simprints RAMP datastore config" diff --git a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt index aeb92a9f30..d9ce5e3cf3 100644 --- a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt +++ b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt @@ -82,6 +82,35 @@ class RampDatastoreRepositoryTest { } } + @Test + fun `isSearchEnabled should return false only when program disables search`() { + stubRampConfigRawValue( + """ + { + "programSpecificSettings": [ + { + "programId": "disabledProgram", + "isSearchEnabled": false + }, + { + "programId": "defaultProgram" + }, + { + "programId": "enabledProgram", + "isSearchEnabled": true + } + ] + } + """.trimIndent(), + ) + + assertEquals(false, repository.isSearchEnabled("disabledProgram")) + assertEquals(true, repository.isSearchEnabled("enabledProgram")) + assertEquals(true, repository.isSearchEnabled("defaultProgram")) + assertEquals(true, repository.isSearchEnabled("missingProgram")) + assertEquals(true, repository.isSearchEnabled(null)) + } + @Test fun `getConfig should parse wrapped string datastore value`() { val rawJson = From 62b63925da10ac2382a002838b367f0857430019 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Jun 2026 17:51:44 +0100 Subject: [PATCH 14/18] Simprints RAMP: README references updated --- README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 080be19de5..eda9084a79 100644 --- a/README.md +++ b/README.md @@ -78,33 +78,46 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | RAMP Infra: version info | [AboutFragment.kt#L45](app/src/main/java/org/dhis2/usescases/about/AboutFragment.kt#L45) | Code addition | Binds the RAMPcapture version text while keeping the DHIS2 app and SDK version | | RAMP Infra: version info | [strings.xml#L363](app/src/main/res/values/strings.xml#L363) | Code addition | Adds the `RAMPcapture Version: %s` label for the new About screen row | | RAMP ACF-1,39,40,41: datastore config | [RampDatastoreConfig.kt#L5](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L5) | New file | Defines the RAMP datastore config schema for enabled data-element history charts and program-stage history tables | -| RAMP ACF-1,39,40,41: datastore config | [RampDatastoreRepository.kt#L13](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L13) | New file | Reads, validates, caches, and downloads the `simprints/ramp` DHIS2 datastore config used by RAMP features | +| RAMP ACF-1,39,40,41: datastore config | [RampDatastoreConfig.kt#L27](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L27) | Code addition | Adds optional per-chart decimal-place limits to the datastore config | +| RAMP ACF-1,39,40,41: datastore config | [RampDatastoreRepository.kt#L14](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L14) | New file | Reads, validates, caches, and downloads the `simprints/ramp` DHIS2 datastore config used by RAMP features | | RAMP ACF-1,39,40,41: datastore config | [SyncPresenterImpl.kt#L272](app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt#L272) | Code addition | Syncs the RAMP datastore config after metadata download so charts and tables use current server configuration | | RAMP ACF-1: field history charts | [EventRepository.kt#L776](form/src/main/java/org/dhis2/form/data/EventRepository.kt#L776) | Code addition | Attaches a configured RAMP history chart to matching event fields when form items load or rules update fields | | RAMP ACF-1: field history charts | [FormHistoryChartRepository.kt#L10](form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt#L10) | New file | Builds per-field visit history chart data from the current enrollment's configured follow-up visit events | -| RAMP ACF-1: field history charts | [FormHistoryChartView.kt#L48](form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt#L48) | New file | Renders the configured field history chart as a line chart with visit-number X-axis labels | +| RAMP ACF-1: field history charts | [FormHistoryChart.kt#L8](form/src/main/java/org/dhis2/form/simprints/ramp/model/FormHistoryChart.kt#L8) | Code addition | Carries the configured decimal-place limit with chart state | +| RAMP ACF-1: field history charts | [FormHistoryChartView.kt#L49](form/src/main/java/org/dhis2/form/simprints/ramp/ui/FormHistoryChartView.kt#L49) | New file | Renders the configured field history chart as a line chart with visit-number X-axis labels | | RAMP ACF-1: field history charts | [FieldProvider.kt#L164](form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt#L164) | Code change | Displays the RAMP history chart directly under the matching input field | | RAMP ACF-1: field history charts | [FormViewModel.kt#L313](form/src/main/java/org/dhis2/form/ui/FormViewModel.kt#L313) | Code change | Keeps the plotted current-event value live while the user edits a field | | RAMP ACF-39,40,41: history tables | [EventHistoryTableRepository.kt#L20](app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt#L20) | New file | Builds visit-column history tables from configured follow-up visit events, grouped by program-stage sections | +| RAMP ACF-39,40,41: history tables | [EventHistoryTableRepository.kt#L212](app/src/main/java/org/dhis2/simprints/ramp/data/EventHistoryTableRepository.kt#L212) | Code change | Orders table rows to follow the configured program-stage section data-element order | | RAMP ACF-39,40,41: history tables | [EventHistoryTableScreen.kt#L41](app/src/main/java/org/dhis2/simprints/ramp/ui/EventHistoryTableScreen.kt#L41) | New file | Renders the history table with visit columns, date row, section headers, loading, empty, and error states | | RAMP ACF-39,40,41: history tables | [EventHistoryTableFragment.kt#L23](app/src/main/java/org/dhis2/simprints/ramp/ui/EventHistoryTableFragment.kt#L23) | New file | Shows the history table in event-capture and TEI dashboard contexts using event or enrollment arguments | | RAMP ACF-39,40,41: history tables | [EventCaptureRepositoryImpl.java#L217](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java#L217) | Code addition | Detects whether the current event's program stage has a configured RAMP history table | -| RAMP ACF-39,40,41: history tables | [EventCapturePagerAdapter.kt#L69](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt#L69) | Code change | Adds the event-capture History page and maps it to the table navigation item | -| RAMP ACF-39,40,41: history tables | [EventCapturePresenterImpl.kt#L155](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt#L155) | Code change | Adds the RAMP History navigation item to program stage (event) screens (where configured) | +| RAMP ACF-39,40,41: history tables | [EventCapturePagerAdapter.kt#L69](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt#L69) | Code change | Adds the event-capture table page and maps it to the RAMP Chart navigation item | +| RAMP ACF-39,40,41: history tables | [EventCapturePresenterImpl.kt#L155](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt#L155) | Code change | Adds the RAMP Chart navigation item to program stage (event) screens (where configured) | +| RAMP ACF-39,40,41: history tables | [EventPageConfigurator.kt#L17](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt#L17) | Code change | Replaces Notes with the RAMP table page only when a program-stage history table is configured | | RAMP ACF-39,40,41: history tables | [EventCaptureActivity.kt#L237](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt#L237) | Code change | Expands the event-capture history table to full width in landscape and restores the split layout afterwards | | RAMP ACF-39,40,41: history tables | [DashboardRepositoryImpl.kt#L817](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt#L817) | Code addition | Detects whether the current enrollment program has a configured RAMP history table | -| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L176](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L176) | Code addition | Adds the RAMP History navigation item to program enrollment screens (where configured) | +| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L176](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L176) | Code addition | Adds the RAMP Chart navigation item to program enrollment screens (where configured) | +| RAMP ACF-39,40,41: history tables | [TeiDashboardPageConfigurator.kt#L15](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt#L15) | Code change | Replaces Notes with the RAMP table page only when an enrollment history table is configured | | RAMP ACF-39,40,41: history tables | [TeiDashboardMobileActivity.kt#L417](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt#L417) | Code addition | Opens the enrollment history table from the TEI dashboard and expands it to full width in landscape (to use space on tablets) | +| RAMP Search restriction in a program | [RampDatastoreConfig.kt#L8](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L8) | Code addition | Adds `programSpecificSettings` to the RAMP datastore schema, with per-program `isSearchEnabled` settings | +| RAMP Search restriction in a program | [SearchRepositoryImpl.java#L1020](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java#L1020) | Code addition | Resolves the current program's search-enabled flag through the RAMP datastore repository | +| RAMP Search restriction in a program | [SearchTEIViewModel.kt#L220](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt#L220) | Code change | Loads the restriction into screen state, prevents opening search, and suppresses initial search prompts when disabled | +| RAMP Search restriction in a program | [SearchTEUi.kt#L244](app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt#L244) | Code change | Lets shared search/create controls hide search actions while preserving create actions where allowed | +| RAMP Search restriction in a program | [SearchTEList.kt#L226](app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt#L226) | Code change | Applies the restriction to the list screen's search controls | +| RAMP Search restriction in a program | [SearchTEMap.kt#L145](app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt#L145) | Code change | Applies the restriction to the map screen's search action | +| RAMP Search restriction in a program | [SearchJavaToCompose.kt#L22](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt#L22) | Code change | Hides the landscape open-search button when the selected program disables search | +| RAMP Search restriction in a program | [SearchScreenConfigurator.kt#L54](app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt#L54) | Code change | Keeps the landscape search panel closed when search is disabled while still allowing filters to open | ### Releases -SimCapture [releases](https://github.com/Simprints/SimCapture/releases) have the following naming -scheme: `SimCapture-DHIS2--fork-`. -An example of a release APK name: `SimCapture-DHIS2-v3.3.1-fork-1-signed-release.apk`. +RAMPcapture [releases](https://github.com/Simprints/SimCapture/releases) have the following naming +scheme: `RAMPcapture-DHIS2--fork-`. +An example of a release APK name: `RAMPcapture-DHIS2-v3.3.1-fork-1-signed-release.apk`. ### GitHub Actions -The workflows of this fork repo should start with `simcapture-`. +The workflows of this fork repo should start with `simcapture-` or `rampcapture-`. The upstream `README` is preserved below. From e28b4cca645018e0e498519028744891551f03b9 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 23 Jun 2026 17:16:33 +0100 Subject: [PATCH 15/18] Simprints RAMP: ACF-39,40,41 table (Chart) tabs are now the only ones besides Form/Details. Forced full-width view on Chart tab item press on landscape --- README.md | 10 +-- .../eventCapture/EventCapturePresenterImpl.kt | 12 +++- .../eventCapture/EventPageConfigurator.kt | 2 +- .../teiDashboard/DashboardViewModel.kt | 17 +++-- .../TeiDashboardMobileActivity.kt | 30 ++++++--- .../TeiDashboardPageConfigurator.kt | 2 +- .../eventCapture/EventCapturePresenterTest.kt | 63 +++++++++++++++++++ .../eventCapture/EventPageConfiguratorTest.kt | 16 +++++ .../teiDashboard/DashboardViewModelTest.kt | 54 ++++++++++------ .../TeiDashboardPageConfiguratorTest.kt | 10 ++- 10 files changed, 170 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index eda9084a79..0ff07e27ae 100644 --- a/README.md +++ b/README.md @@ -93,13 +93,13 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | RAMP ACF-39,40,41: history tables | [EventHistoryTableFragment.kt#L23](app/src/main/java/org/dhis2/simprints/ramp/ui/EventHistoryTableFragment.kt#L23) | New file | Shows the history table in event-capture and TEI dashboard contexts using event or enrollment arguments | | RAMP ACF-39,40,41: history tables | [EventCaptureRepositoryImpl.java#L217](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java#L217) | Code addition | Detects whether the current event's program stage has a configured RAMP history table | | RAMP ACF-39,40,41: history tables | [EventCapturePagerAdapter.kt#L69](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt#L69) | Code change | Adds the event-capture table page and maps it to the RAMP Chart navigation item | -| RAMP ACF-39,40,41: history tables | [EventCapturePresenterImpl.kt#L155](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt#L155) | Code change | Adds the RAMP Chart navigation item to program stage (event) screens (where configured) | -| RAMP ACF-39,40,41: history tables | [EventPageConfigurator.kt#L17](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt#L17) | Code change | Replaces Notes with the RAMP table page only when a program-stage history table is configured | +| RAMP ACF-39,40,41: history tables | [EventCapturePresenterImpl.kt#L167](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt#L167) | Code change | Keeps the RAMP Chart navigation item visible as a single tab when no other right-pane tabs are configured | +| RAMP ACF-39,40,41: history tables | [EventPageConfigurator.kt#L13](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt#L13) | Code change | Hides Analytics and Notes when a program-stage history table is configured, leaving only Form and Chart | | RAMP ACF-39,40,41: history tables | [EventCaptureActivity.kt#L237](app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt#L237) | Code change | Expands the event-capture history table to full width in landscape and restores the split layout afterwards | | RAMP ACF-39,40,41: history tables | [DashboardRepositoryImpl.kt#L817](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt#L817) | Code addition | Detects whether the current enrollment program has a configured RAMP history table | -| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L176](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L176) | Code addition | Adds the RAMP Chart navigation item to program enrollment screens (where configured) | -| RAMP ACF-39,40,41: history tables | [TeiDashboardPageConfigurator.kt#L15](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt#L15) | Code change | Replaces Notes with the RAMP table page only when an enrollment history table is configured | -| RAMP ACF-39,40,41: history tables | [TeiDashboardMobileActivity.kt#L417](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt#L417) | Code addition | Opens the enrollment history table from the TEI dashboard and expands it to full width in landscape (to use space on tablets) | +| RAMP ACF-39,40,41: history tables | [DashboardViewModel.kt#L177](app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt#L177) | Code addition | Adds the RAMP Chart navigation item and only forces Details as the landscape fullscreen restore tab | +| RAMP ACF-39,40,41: history tables | [TeiDashboardPageConfigurator.kt#L11](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt#L11) | Code change | Hides Analytics and Notes when an enrollment history table is configured, leaving only Details and Chart | +| RAMP ACF-39,40,41: history tables | [TeiDashboardMobileActivity.kt#L369](app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt#L369) | Code addition | Expands the enrollment history table only after Chart is pressed in landscape, with Details as restore tab | | RAMP Search restriction in a program | [RampDatastoreConfig.kt#L8](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L8) | Code addition | Adds `programSpecificSettings` to the RAMP datastore schema, with per-program `isSearchEnabled` settings | | RAMP Search restriction in a program | [SearchRepositoryImpl.java#L1020](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java#L1020) | Code addition | Resolves the current program's search-enabled flag through the RAMP datastore repository | | RAMP Search restriction in a program | [SearchTEIViewModel.kt#L220](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt#L220) | Code change | Loads the restriction into screen state, prevents opening search, and suppresses initial search prompts when disabled | diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt index 7c97b28603..2ec9614f38 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt @@ -107,6 +107,7 @@ class EventCapturePresenterImpl( private fun loadBottomBarItems() { val navItems = mutableListOf>() + val displayTableView = pageConfigurator.displayTableView() if (pageConfigurator.displayDataEntry() || forceDisplayDataEntryNavigationItemForSimprintsRampTable) { navItems.add( @@ -152,7 +153,7 @@ class EventCapturePresenterImpl( ) } - if (pageConfigurator.displayTableView()) { + if (displayTableView) { navItems.add( NavigationBarItem( id = NavigationPage.TABLE_VIEW, @@ -163,9 +164,16 @@ class EventCapturePresenterImpl( ) } + val visibleItems = navItems.takeIf { it.size > 1 || displayTableView }.orEmpty() + val selectedItem = + navigationBarUIState.value.selectedItem + ?.takeIf { item -> visibleItems.any { it.id == item } } + ?: visibleItems.firstOrNull()?.id + navigationBarUIState.value = navigationBarUIState.value.copy( - items = navItems.takeIf { it.size > 1 }.orEmpty(), + items = visibleItems, + selectedItem = selectedItem, ) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt index ab1159f2d5..7c7033af45 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt @@ -10,7 +10,7 @@ class EventPageConfigurator( override fun displayDataEntry(): Boolean = isPortrait - override fun displayAnalytics(): Boolean = eventCaptureRepository.hasAnalytics() + override fun displayAnalytics(): Boolean = eventCaptureRepository.hasAnalytics() && !displayTableView() override fun displayRelationships(): Boolean = eventCaptureRepository.hasRelationships() diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index 112c898a97..807db06e25 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -49,6 +49,7 @@ class DashboardViewModel( private val selectedEventUid = MutableLiveData() private var displayDetailsNavigationItem = pageConfigurator.displayDetails() + private var forceDisplayDetailsNavigationItemForSimprintsRampTable = false val showStatusErrorMessages = MutableLiveData(StatusChangeResultCode.CHANGED) @@ -129,7 +130,7 @@ class DashboardViewModel( private fun updateNavigationBarItems(selectedItem: TEIDashboardItems?) { val enrollmentItems = mutableListOf>() - if (displayDetailsNavigationItem || selectedItem == TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) { + if (displayDetailsNavigationItem || forceDisplayDetailsNavigationItemForSimprintsRampTable) { enrollmentItems.add( NavigationBarItem( id = TEIDashboardItems.DETAILS, @@ -316,14 +317,12 @@ class DashboardViewModel( } } - fun getDefaultLandscapeNavigationItemExceptHistory(): TEIDashboardItems? = - _navigationBarUIState - .value - .items - .firstOrNull { - it.id != TEIDashboardItems.DETAILS && // not in tab bar in landscape - it.id != TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE - }?.id + fun setForceDisplayDetailsNavigationItemForSimprintsRampTable(forceDisplay: Boolean) { + if (forceDisplayDetailsNavigationItemForSimprintsRampTable != forceDisplay) { + forceDisplayDetailsNavigationItemForSimprintsRampTable = forceDisplay + updateNavigationBarItems(_navigationBarUIState.value.selectedItem) + } + } fun checkIfTeiCanBeTransferred(): Boolean = repository.teiCanBeTransferred() 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 58ec29e1b6..b1d9a09199 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -360,22 +360,38 @@ class TeiDashboardMobileActivity : } private fun onNavigationBarItemSelected(item: TEIDashboardItems) { - if (isLandscape() && item == TEIDashboardItems.DETAILS) { - restoreSimprintsRampHistoryTableLandscapeLayoutAndSelectDefaultPage() + if (isLandscape()) { + when (item) { + TEIDashboardItems.DETAILS -> { + restoreSimprintsRampHistoryTableLandscapeLayout() + } + + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE -> { + setSimprintsRampHistoryTableLandscapeFullscreen(true) + dashboardViewModel.setForceDisplayDetailsNavigationItemForSimprintsRampTable(true) + dashboardViewModel.onNavigationItemSelected(item) + } + + else -> { + setSimprintsRampHistoryTableLandscapeFullscreen(false) + dashboardViewModel.setForceDisplayDetailsNavigationItemForSimprintsRampTable(false) + dashboardViewModel.onNavigationItemSelected(item) + } + } } else { dashboardViewModel.onNavigationItemSelected(item) } } - private fun restoreSimprintsRampHistoryTableLandscapeLayoutAndSelectDefaultPage() { + private fun restoreSimprintsRampHistoryTableLandscapeLayout() { setSimprintsRampHistoryTableLandscapeFullscreen(false) - dashboardViewModel - .getDefaultLandscapeNavigationItemExceptHistory() - ?.let(dashboardViewModel::onNavigationItemSelected) + dashboardViewModel.setForceDisplayDetailsNavigationItemForSimprintsRampTable(false) } private fun navigateToFragment(item: TEIDashboardItems) { - setSimprintsRampHistoryTableLandscapeFullscreen(item == TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) + if (item != TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) { + setSimprintsRampHistoryTableLandscapeFullscreen(false) + } val fragment = when (item) { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt index cbea64ca7d..1ac18b7cfc 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfigurator.kt @@ -8,7 +8,7 @@ class TeiDashboardPageConfigurator( ) : NavigationPageConfigurator { override fun displayDetails(): Boolean = isPortrait - override fun displayAnalytics(): Boolean = dashboardRepository.programHasAnalytics() + override fun displayAnalytics(): Boolean = dashboardRepository.programHasAnalytics() && !displayTableView() override fun displayRelationships(): Boolean = dashboardRepository.programHasRelationships() diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt index 1637e661cb..45cd124ea5 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterTest.kt @@ -4,14 +4,22 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.data.schedulers.TrampolineSchedulerProvider +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.common.ValidationStrategy import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.ProgramStage +import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -26,10 +34,12 @@ import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import java.util.Date +@OptIn(ExperimentalCoroutinesApi::class) class EventCapturePresenterTest { @get:Rule val rule = InstantTaskExecutorRule() + private val testingDispatcher = UnconfinedTestDispatcher() private lateinit var presenter: EventCapturePresenterImpl private val view: EventCaptureContract.View = mock() private val eventUid = "eventUid" @@ -41,6 +51,8 @@ class EventCapturePresenterTest { @Before fun setUp() { + Dispatchers.setMain(testingDispatcher) + whenever(resourceManager.getString(any())) doReturn "" presenter = EventCapturePresenterImpl( view, @@ -53,6 +65,11 @@ class EventCapturePresenterTest { ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun `Should initialize the event capture form`() { initializeMocks() @@ -293,6 +310,52 @@ class EventCapturePresenterTest { verify(view).updateNoteBadge(1) } + @Test + fun `Should display one chart navigation item when only Simprints RAMP history table tab is configured`() { + initializeMocks() + whenever(eventRepository.eventIntegrityCheck()) doReturn Flowable.just(true) + whenever(eventRepository.eventStatus()) doReturn Flowable.just(EventStatus.ACTIVE) + whenever(eventRepository.isEventEditable("eventUid")) doReturn true + whenever(pageConfigurator.displayDataEntry()) doReturn false + whenever(pageConfigurator.displayAnalytics()) doReturn false + whenever(pageConfigurator.displayRelationships()) doReturn false + whenever(pageConfigurator.displayNotes()) doReturn false + whenever(pageConfigurator.displayTableView()) doReturn true + + presenter.init() + + assertEquals( + listOf(NavigationPage.TABLE_VIEW), + presenter.observeNavigationBarUIState().value.items.map { it.id }, + ) + assertEquals( + NavigationPage.TABLE_VIEW, + presenter.observeNavigationBarUIState().value.selectedItem, + ) + + presenter.setForceDisplayDataEntryNavigationItemForSimprintsRampTable(true) + + assertEquals( + listOf(NavigationPage.DATA_ENTRY, NavigationPage.TABLE_VIEW), + presenter.observeNavigationBarUIState().value.items.map { it.id }, + ) + assertEquals( + NavigationPage.TABLE_VIEW, + presenter.observeNavigationBarUIState().value.selectedItem, + ) + + presenter.setForceDisplayDataEntryNavigationItemForSimprintsRampTable(false) + + assertEquals( + listOf(NavigationPage.TABLE_VIEW), + presenter.observeNavigationBarUIState().value.items.map { it.id }, + ) + assertEquals( + NavigationPage.TABLE_VIEW, + presenter.observeNavigationBarUIState().value.selectedItem, + ) + } + private fun initializeMocks() { val stage = ProgramStage diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt index 9579f7f383..87f0538189 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfiguratorTest.kt @@ -25,6 +25,22 @@ class EventPageConfiguratorTest { assertFalse(pageConfigurator.displayTableView()) } + @Test + fun `displayAnalytics should be true when analytics are configured and Simprints RAMP history table is not configured`() { + whenever(eventCaptureRepository.hasAnalytics()) doReturn true + whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn false + + assertTrue(pageConfigurator.displayAnalytics()) + } + + @Test + fun `displayAnalytics should be false when Simprints RAMP history table is configured`() { + whenever(eventCaptureRepository.hasAnalytics()) doReturn true + whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn true + + assertFalse(pageConfigurator.displayAnalytics()) + } + @Test fun `displayNotes should be true when Simprints RAMP history table is not configured`() { whenever(eventCaptureRepository.hasSimprintsRampProgramStageHistoryTable()) doReturn false diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt index 064b3d8ba0..64b033011e 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt @@ -17,7 +17,7 @@ import org.dhis2.tracker.TEIDashboardItems import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus -import org.junit.Assert.assertFalse +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -105,41 +105,55 @@ class DashboardViewModelTest { } @Test - fun shouldDisplayDetailsWhenHistoryIsSelectedInLandscape() { + fun shouldDisplayOnlyHistoryUntilLandscapeHistoryFullscreenIsForced() { mockEnrollmentModel() mockGrouping(false) whenever(pageConfigurator.displayDetails()) doReturn false - whenever(pageConfigurator.displayAnalytics()) doReturn true + whenever(pageConfigurator.displayAnalytics()) doReturn false whenever(pageConfigurator.displayTableView()) doReturn true val dashboardViewModel = getViewModel() - assertTrue( - dashboardViewModel.navigationItemIds() == - listOf( - TEIDashboardItems.ANALYTICS, - TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, - ), + assertEquals( + listOf(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE), + dashboardViewModel.navigationItemIds(), + ) + assertEquals( + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + dashboardViewModel.navigationBarUIState.value.selectedItem, ) dashboardViewModel.onNavigationItemSelected(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE) - assertTrue( - dashboardViewModel.navigationItemIds() == - listOf( - TEIDashboardItems.DETAILS, - TEIDashboardItems.ANALYTICS, - TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, - ), + assertEquals( + listOf(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE), + dashboardViewModel.navigationItemIds(), ) - assertTrue( - dashboardViewModel.navigationBarUIState.value.selectedItem == + + dashboardViewModel.setForceDisplayDetailsNavigationItemForSimprintsRampTable(true) + + assertEquals( + listOf( + TEIDashboardItems.DETAILS, TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + ), + dashboardViewModel.navigationItemIds(), + ) + assertEquals( + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + dashboardViewModel.navigationBarUIState.value.selectedItem, ) - dashboardViewModel.onNavigationItemSelected(TEIDashboardItems.ANALYTICS) + dashboardViewModel.setForceDisplayDetailsNavigationItemForSimprintsRampTable(false) - assertFalse(dashboardViewModel.navigationItemIds().contains(TEIDashboardItems.DETAILS)) + assertEquals( + listOf(TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE), + dashboardViewModel.navigationItemIds(), + ) + assertEquals( + TEIDashboardItems.SIMPRINTS_RAMP_HISTORY_TABLE, + dashboardViewModel.navigationBarUIState.value.selectedItem, + ) } @Test diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt index a12042d581..3182d09d7f 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/TeiDashboardPageConfiguratorTest.kt @@ -20,8 +20,9 @@ class TeiDashboardPageConfiguratorTest { } @Test - fun `Should display analytics screen if the program is configured`() { + fun `Should display analytics screen if the program is configured and Simprints RAMP history screen is not configured`() { whenever(dashboardRepository.programHasAnalytics()) doReturn true + whenever(dashboardRepository.programHasSimprintsRampProgramStageHistoryTable()) doReturn false assertTrue(pageConfigurator.displayAnalytics()) } @@ -31,6 +32,13 @@ class TeiDashboardPageConfiguratorTest { assertTrue(!pageConfigurator.displayAnalytics()) } + @Test + fun `Should not display analytics screen if Simprints RAMP history screen is configured`() { + whenever(dashboardRepository.programHasAnalytics()) doReturn true + whenever(dashboardRepository.programHasSimprintsRampProgramStageHistoryTable()) doReturn true + assertFalse(pageConfigurator.displayAnalytics()) + } + @Test fun `Should display relationships screen if the program is configured`() { whenever(dashboardRepository.programHasRelationships()) doReturn true From 7bfa76e9a85a06381c113f480dd6d7118c19baab Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Jun 2026 13:55:23 +0100 Subject: [PATCH 16/18] Simprints RAMP: Refer and Schedule "+" menu options removable from program stage via config --- README.md | 3 ++ .../teidata/TEIDataModule.kt | 11 ++++- .../domain/GetNewEventCreationTypeOptions.kt | 12 +++++- .../GetNewEventCreationTypeOptionsTest.kt | 20 +++++++++- .../ramp/model/RampDatastoreConfig.kt | 15 ++++++- .../repository/RampDatastoreRepository.kt | 31 +++++++++++++- .../repository/RampDatastoreRepositoryTest.kt | 40 +++++++++++++++++++ 7 files changed, 126 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ff07e27ae..fd57fd9be1 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | RAMP Search restriction in a program | [SearchTEMap.kt#L145](app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt#L145) | Code change | Applies the restriction to the map screen's search action | | RAMP Search restriction in a program | [SearchJavaToCompose.kt#L22](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt#L22) | Code change | Hides the landscape open-search button when the selected program disables search | | RAMP Search restriction in a program | [SearchScreenConfigurator.kt#L54](app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt#L54) | Code change | Keeps the landscape search panel closed when search is disabled while still allowing filters to open | +| RAMP Menu option off in a program stage | [RampDatastoreConfig.kt#L9](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L9) | Code addition | Adds `programStageSpecificSettings` to the RAMP datastore schema, with per-stage schedule and refer option flags | +| RAMP Menu option off in a program stage | [RampDatastoreRepository.kt#L56](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L56) | Code addition | Resolves per-stage schedule and refer option flags from the RAMP datastore | +| RAMP Menu option off in a program stage | [GetNewEventCreationTypeOptions.kt#L21](app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt#L21) | Code change | Applies datastore-controlled stage option flags when building the tracker dashboard add-event menu | ### Releases 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 484f9a05b8..1feec813cb 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 @@ -14,6 +14,7 @@ import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.DhisEnrollmentUtils import org.dhis2.data.forms.dataentry.SearchTEIRepository @@ -129,7 +130,15 @@ class TEIDataModule( @Provides fun provideGetNewEventCreationTypeOptions( programConfigurationRepository: ProgramConfigurationRepository, - ): GetNewEventCreationTypeOptions = GetNewEventCreationTypeOptions(programConfigurationRepository) + rampDatastoreRepository: RampDatastoreRepository, + ): GetNewEventCreationTypeOptions = + GetNewEventCreationTypeOptions( + programConfigurationRepository, + rampDatastoreRepository, + ) + + @Provides + fun provideRampDatastoreRepository(d2: D2): RampDatastoreRepository = RampDatastoreRepository(d2) @Provides fun provideEventCreationsOptionsMapper(resourceManager: ResourceManager): EventCreationOptionsMapper = diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt index 2bdafd67e4..f66b777e07 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt @@ -5,10 +5,12 @@ import org.dhis2.commons.data.EventCreationType.ADDNEW import org.dhis2.commons.data.EventCreationType.REFERAL import org.dhis2.commons.data.EventCreationType.SCHEDULE import org.dhis2.commons.data.ProgramConfigurationRepository +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.hisp.dhis.android.core.program.ProgramStage class GetNewEventCreationTypeOptions( private val programConfigurationRepository: ProgramConfigurationRepository, + private val rampDatastoreRepository: RampDatastoreRepository, ) { operator fun invoke( programStage: ProgramStage?, @@ -16,10 +18,16 @@ class GetNewEventCreationTypeOptions( ): List { val options: MutableList = mutableListOf() options.add(ADDNEW) - if (programStage == null || shouldShowScheduleEvents(programStage)) { + if ( + (programStage == null || shouldShowScheduleEvents(programStage)) && + rampDatastoreRepository.isScheduleOptionEnabled(programStage?.uid()) + ) { options.add(SCHEDULE) } - if (shouldShowReferralEvents(programUid)) { + if ( + shouldShowReferralEvents(programUid) && + rampDatastoreRepository.isReferOptionEnabled(programStage?.uid()) + ) { options.add(REFERAL) } return options diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptionsTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptionsTest.kt index 74935d5622..00a2d6a916 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptionsTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptionsTest.kt @@ -1,14 +1,18 @@ package org.dhis2.usescases.teiDashboard.domain +import org.dhis2.commons.data.EventCreationType.ADDNEW import org.dhis2.commons.data.EventCreationType.REFERAL import org.dhis2.commons.data.EventCreationType.SCHEDULE import org.dhis2.commons.data.ProgramConfigurationRepository +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.settings.ProgramConfigurationSetting +import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -22,6 +26,7 @@ class GetNewEventCreationTypeOptionsTest { mock { on { getConfigurationByProgram(programUid) } doReturn programConfigurationSetting } + private val rampDatastoreRepository: RampDatastoreRepository = mock() val programStage: ProgramStage = mock() @@ -29,8 +34,10 @@ class GetNewEventCreationTypeOptionsTest { @Before fun setUp() { + whenever(rampDatastoreRepository.isScheduleOptionEnabled(anyOrNull())) doReturn true + whenever(rampDatastoreRepository.isReferOptionEnabled(anyOrNull())) doReturn true getNewEventCreationTypeOptions = - GetNewEventCreationTypeOptions(programConfigurationRepository) + GetNewEventCreationTypeOptions(programConfigurationRepository, rampDatastoreRepository) } @Test @@ -86,4 +93,15 @@ class GetNewEventCreationTypeOptionsTest { assertNull(result.find { it == REFERAL }) } + + @Test + fun shouldOnlyReturnAddNewTypeWhenStageOptionsAreDisabled() { + whenever(programStage.uid()) doReturn "programStageUid" + whenever(rampDatastoreRepository.isScheduleOptionEnabled("programStageUid")) doReturn false + whenever(rampDatastoreRepository.isReferOptionEnabled("programStageUid")) doReturn false + + val result = getNewEventCreationTypeOptions.invoke(programStage, programUid) + + assertEquals(listOf(ADDNEW), result) + } } diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt index 89e3b7f9f3..c9e808c88d 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt @@ -6,11 +6,13 @@ data class RampDatastoreConfig( val dataElementHistoryCharts: List = emptyList(), val programStageHistoryTables: List = emptyList(), val programSpecificSettings: List = emptyList(), + val programStageSpecificSettings: List = emptyList(), ) { fun isNotEmpty(): Boolean = dataElementHistoryCharts.isNotEmpty() || programStageHistoryTables.isNotEmpty() || - programSpecificSettings.isNotEmpty() + programSpecificSettings.isNotEmpty() || + programStageSpecificSettings.isNotEmpty() } data class DataElementHistoryChartConfig( @@ -62,3 +64,14 @@ data class ProgramSpecificSetting( ) { fun isValid(): Boolean = !programId.isNullOrBlank() } + +data class ProgramStageSpecificSetting( + @SerializedName("programStageId") + val programStageId: String? = null, + @SerializedName("isScheduleOptionEnabled") + val isScheduleOptionEnabled: Boolean? = null, + @SerializedName("isReferOptionEnabled") + val isReferOptionEnabled: Boolean? = null, +) { + fun isValid(): Boolean = !programStageId.isNullOrBlank() +} diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt index 944ee528f1..fa4e73c89b 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt @@ -5,8 +5,9 @@ import com.google.gson.JsonElement import com.google.gson.JsonParseException import com.google.gson.JsonParser import org.dhis2.commons.simprints.ramp.model.DataElementHistoryChartConfig -import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig import org.dhis2.commons.simprints.ramp.model.ProgramSpecificSetting +import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig +import org.dhis2.commons.simprints.ramp.model.ProgramStageSpecificSetting import org.dhis2.commons.simprints.ramp.model.RampDatastoreConfig import org.hisp.dhis.android.core.D2 import timber.log.Timber @@ -52,6 +53,27 @@ class RampDatastoreRepository( } } + fun isScheduleOptionEnabled(programStageId: String?): Boolean = isProgramStageOptionEnabled(programStageId) { isScheduleOptionEnabled } + + fun isReferOptionEnabled(programStageId: String?): Boolean = isProgramStageOptionEnabled(programStageId) { isReferOptionEnabled } + + private fun isProgramStageOptionEnabled( + programStageId: String?, + option: ProgramStageSpecificSetting.() -> Boolean?, + ): Boolean { + if (programStageId.isNullOrBlank()) return true + + return try { + getConfig() + .programStageSpecificSettings + .firstOrNull { it.programStageId == programStageId } + ?.option() != false + } catch (exception: RuntimeException) { + Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) + true + } + } + private fun getLocalRampDatastoreValue(): String? = d2 .dataStoreModule() @@ -83,6 +105,12 @@ class RampDatastoreRepository( ?.parseList() .orEmpty() .filter { it.isValid() }, + programStageSpecificSettings = + root + .get(PROGRAM_STAGE_SPECIFIC_SETTINGS_KEY) + ?.parseList() + .orEmpty() + .filter { it.isValid() }, ) } catch (exception: JsonParseException) { Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) @@ -155,6 +183,7 @@ class RampDatastoreRepository( private const val DATA_ELEMENT_HISTORY_CHARTS_KEY = "dataElementHistoryCharts" private const val PROGRAM_STAGE_HISTORY_TABLE_KEY = "programStageHistoryTable" private const val PROGRAM_SPECIFIC_SETTINGS_KEY = "programSpecificSettings" + private const val PROGRAM_STAGE_SPECIFIC_SETTINGS_KEY = "programStageSpecificSettings" private const val DATASTORE_JSON_WRAPPER_NAME = "JsonWrapper" private const val DATASTORE_JSON_WRAPPER_JSON_FIELD = "json" private const val RAMP_DATASTORE_PARSE_ERROR = "Failed to parse Simprints RAMP datastore config" diff --git a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt index d9ce5e3cf3..3272127885 100644 --- a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt +++ b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt @@ -111,6 +111,46 @@ class RampDatastoreRepositoryTest { assertEquals(true, repository.isSearchEnabled(null)) } + @Test + fun `program stage options should return false only when stage disables them`() { + stubRampConfigRawValue( + """ + { + "programStageSpecificSettings": [ + { + "programStageId": "disabledStage", + "isScheduleOptionEnabled": false, + "isReferOptionEnabled": false + }, + { + "programStageId": "defaultStage" + }, + { + "programStageId": "enabledStage", + "isScheduleOptionEnabled": true, + "isReferOptionEnabled": true + }, + { + "isScheduleOptionEnabled": false, + "isReferOptionEnabled": false + } + ] + } + """.trimIndent(), + ) + + assertEquals(false, repository.isScheduleOptionEnabled("disabledStage")) + assertEquals(false, repository.isReferOptionEnabled("disabledStage")) + assertEquals(true, repository.isScheduleOptionEnabled("enabledStage")) + assertEquals(true, repository.isReferOptionEnabled("enabledStage")) + assertEquals(true, repository.isScheduleOptionEnabled("defaultStage")) + assertEquals(true, repository.isReferOptionEnabled("defaultStage")) + assertEquals(true, repository.isScheduleOptionEnabled("missingStage")) + assertEquals(true, repository.isReferOptionEnabled("missingStage")) + assertEquals(true, repository.isScheduleOptionEnabled(null)) + assertEquals(true, repository.isReferOptionEnabled(null)) + } + @Test fun `getConfig should parse wrapped string datastore value`() { val rawJson = From c6145f1be78b63b7cb9b1e0644c6a8aefbf0d97e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 1 Jul 2026 12:44:58 +0100 Subject: [PATCH 17/18] Simprints RAMP: Consistent resilience against accidental whitespaces in IDs in datastore config --- README.md | 2 +- .../repository/RampDatastoreRepository.kt | 38 +++++++++-- .../repository/RampDatastoreRepositoryTest.kt | 68 ++++++++++++++++++- 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fd57fd9be1..a764b7a894 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | RAMP Infra: version info | [strings.xml#L363](app/src/main/res/values/strings.xml#L363) | Code addition | Adds the `RAMPcapture Version: %s` label for the new About screen row | | RAMP ACF-1,39,40,41: datastore config | [RampDatastoreConfig.kt#L5](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L5) | New file | Defines the RAMP datastore config schema for enabled data-element history charts and program-stage history tables | | RAMP ACF-1,39,40,41: datastore config | [RampDatastoreConfig.kt#L27](commons/src/main/java/org/dhis2/commons/simprints/ramp/model/RampDatastoreConfig.kt#L27) | Code addition | Adds optional per-chart decimal-place limits to the datastore config | -| RAMP ACF-1,39,40,41: datastore config | [RampDatastoreRepository.kt#L14](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L14) | New file | Reads, validates, caches, and downloads the `simprints/ramp` DHIS2 datastore config used by RAMP features | +| RAMP ACF-1,39,40,41: datastore config | [RampDatastoreRepository.kt#L15](commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt#L15) | New file | Reads, validates, caches, and downloads the `simprints/ramp` DHIS2 datastore config used by RAMP features | | RAMP ACF-1,39,40,41: datastore config | [SyncPresenterImpl.kt#L272](app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt#L272) | Code addition | Syncs the RAMP datastore config after metadata download so charts and tables use current server configuration | | RAMP ACF-1: field history charts | [EventRepository.kt#L776](form/src/main/java/org/dhis2/form/data/EventRepository.kt#L776) | Code addition | Attaches a configured RAMP history chart to matching event fields when form items load or rules update fields | | RAMP ACF-1: field history charts | [FormHistoryChartRepository.kt#L10](form/src/main/java/org/dhis2/form/simprints/ramp/data/FormHistoryChartRepository.kt#L10) | New file | Builds per-field visit history chart data from the current enrollment's configured follow-up visit events | diff --git a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt index fa4e73c89b..2d3cf5dba5 100644 --- a/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt +++ b/commons/src/main/java/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepository.kt @@ -40,12 +40,12 @@ class RampDatastoreRepository( } fun isSearchEnabled(programId: String?): Boolean { - if (programId.isNullOrBlank()) return true + val normalizedProgramId = programId.trimToValue() ?: return true return try { getConfig() .programSpecificSettings - .firstOrNull { it.programId == programId } + .firstOrNull { it.programId == normalizedProgramId } ?.isSearchEnabled != false } catch (exception: RuntimeException) { Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) @@ -61,12 +61,12 @@ class RampDatastoreRepository( programStageId: String?, option: ProgramStageSpecificSetting.() -> Boolean?, ): Boolean { - if (programStageId.isNullOrBlank()) return true + val normalizedProgramStageId = programStageId.trimToValue() ?: return true return try { getConfig() .programStageSpecificSettings - .firstOrNull { it.programStageId == programStageId } + .firstOrNull { it.programStageId == normalizedProgramStageId } ?.option() != false } catch (exception: RuntimeException) { Timber.e(exception, RAMP_DATASTORE_PARSE_ERROR) @@ -92,24 +92,28 @@ class RampDatastoreRepository( .get(DATA_ELEMENT_HISTORY_CHARTS_KEY) ?.parseList() .orEmpty() + .map { it.normalized() } .filter { it.isValid() }, programStageHistoryTables = root .get(PROGRAM_STAGE_HISTORY_TABLE_KEY) ?.parseList() .orEmpty() + .map { it.normalized() } .filter { it.isValid() }, programSpecificSettings = root .get(PROGRAM_SPECIFIC_SETTINGS_KEY) ?.parseList() .orEmpty() + .map { it.normalized() } .filter { it.isValid() }, programStageSpecificSettings = root .get(PROGRAM_STAGE_SPECIFIC_SETTINGS_KEY) ?.parseList() .orEmpty() + .map { it.normalized() } .filter { it.isValid() }, ) } catch (exception: JsonParseException) { @@ -172,6 +176,32 @@ class RampDatastoreRepository( null } + private fun DataElementHistoryChartConfig.normalized(): DataElementHistoryChartConfig = + copy( + programId = programId.trimToValue(), + followUpVisitProgramStageId = followUpVisitProgramStageId.trimToValue(), + dataElementId = dataElementId.trimToValue(), + xAxisVisitNumberDataElementId = xAxisVisitNumberDataElementId.trimToValue(), + ) + + private fun ProgramStageHistoryTableConfig.normalized(): ProgramStageHistoryTableConfig = + copy( + programId = programId.trimToValue(), + followUpVisitProgramStageId = followUpVisitProgramStageId.trimToValue(), + headerVisitNumberDataElementId = headerVisitNumberDataElementId.trimToValue(), + excludedFollowUpVisitDataElementIds = + excludedFollowUpVisitDataElementIds + ?.mapNotNull { it.trimToValue() }, + ) + + private fun ProgramSpecificSetting.normalized(): ProgramSpecificSetting = + copy(programId = programId.trimToValue()) + + private fun ProgramStageSpecificSetting.normalized(): ProgramStageSpecificSetting = + copy(programStageId = programStageId.trimToValue()) + + private fun String?.trimToValue(): String? = this?.trim()?.takeIf { it.isNotEmpty() } + private data class CachedConfig( val rawValue: String?, val config: RampDatastoreConfig, diff --git a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt index 3272127885..f134168683 100644 --- a/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt +++ b/commons/src/test/kotlin/org/dhis2/commons/simprints/ramp/repository/RampDatastoreRepositoryTest.kt @@ -82,6 +82,64 @@ class RampDatastoreRepositoryTest { } } + @Test + fun `getConfig should trim configured identifiers across all RAMP config sections`() { + stubRampConfigRawValue( + """ + { + "dataElementHistoryCharts": [ + { + "programId": " program ", + "followUpVisitProgramStageId": " follow-stage ", + "dataElementId": " weight ", + "xAxisVisitNumberDataElementId": " visit-number ", + "followUpVisitMaxNumber": 12 + } + ], + "programStageHistoryTable": [ + { + "programId": " program ", + "followUpVisitProgramStageId": " follow-stage ", + "followUpVisitMaxNumber": 12, + "headerVisitNumberDataElementId": " visit-number ", + "excludedFollowUpVisitDataElementIds": [" excluded ", " "] + } + ], + "programSpecificSettings": [ + { + "programId": " disabledProgram ", + "isSearchEnabled": false + } + ], + "programStageSpecificSettings": [ + { + "programStageId": " disabledStage ", + "isScheduleOptionEnabled": false, + "isReferOptionEnabled": false + } + ] + } + """.trimIndent(), + ) + + val config = repository.getConfig() + + config.dataElementHistoryCharts.single().let { chart -> + assertEquals("program", chart.programId) + assertEquals("follow-stage", chart.followUpVisitProgramStageId) + assertEquals("weight", chart.dataElementId) + assertEquals("visit-number", chart.xAxisVisitNumberDataElementId) + } + config.programStageHistoryTables.single().let { table -> + assertEquals("program", table.programId) + assertEquals("follow-stage", table.followUpVisitProgramStageId) + assertEquals("visit-number", table.headerVisitNumberDataElementId) + assertEquals(listOf("excluded"), table.excludedFollowUpVisitDataElementIds) + } + assertEquals("disabledProgram", config.programSpecificSettings.single().programId) + assertEquals("disabledStage", config.programStageSpecificSettings.single().programStageId) + } + @Test fun `isSearchEnabled should return false only when program disables search`() { stubRampConfigRawValue( @@ -89,7 +147,7 @@ class RampDatastoreRepositoryTest { { "programSpecificSettings": [ { - "programId": "disabledProgram", + "programId": " disabledProgram ", "isSearchEnabled": false }, { @@ -105,10 +163,12 @@ class RampDatastoreRepositoryTest { ) assertEquals(false, repository.isSearchEnabled("disabledProgram")) + assertEquals(false, repository.isSearchEnabled(" disabledProgram ")) assertEquals(true, repository.isSearchEnabled("enabledProgram")) assertEquals(true, repository.isSearchEnabled("defaultProgram")) assertEquals(true, repository.isSearchEnabled("missingProgram")) assertEquals(true, repository.isSearchEnabled(null)) + assertEquals(true, repository.isSearchEnabled(" ")) } @Test @@ -118,7 +178,7 @@ class RampDatastoreRepositoryTest { { "programStageSpecificSettings": [ { - "programStageId": "disabledStage", + "programStageId": " disabledStage ", "isScheduleOptionEnabled": false, "isReferOptionEnabled": false }, @@ -141,6 +201,8 @@ class RampDatastoreRepositoryTest { assertEquals(false, repository.isScheduleOptionEnabled("disabledStage")) assertEquals(false, repository.isReferOptionEnabled("disabledStage")) + assertEquals(false, repository.isScheduleOptionEnabled(" disabledStage ")) + assertEquals(false, repository.isReferOptionEnabled(" disabledStage ")) assertEquals(true, repository.isScheduleOptionEnabled("enabledStage")) assertEquals(true, repository.isReferOptionEnabled("enabledStage")) assertEquals(true, repository.isScheduleOptionEnabled("defaultStage")) @@ -149,6 +211,8 @@ class RampDatastoreRepositoryTest { assertEquals(true, repository.isReferOptionEnabled("missingStage")) assertEquals(true, repository.isScheduleOptionEnabled(null)) assertEquals(true, repository.isReferOptionEnabled(null)) + assertEquals(true, repository.isScheduleOptionEnabled(" ")) + assertEquals(true, repository.isReferOptionEnabled(" ")) } @Test From 0a3f8f48803af94a34f0b18a3f419c97e7a37364 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 1 Jul 2026 13:00:29 +0100 Subject: [PATCH 18/18] Simprints RAMP: RampDatastoreRepository injection --- README.md | 6 +- .../eventCapture/EventCaptureModule.kt | 8 +- .../EventCaptureRepositoryImpl.java | 6 +- .../SearchRepositoryImpl.java | 7 +- .../searchTrackEntity/SearchTEModule.java | 15 ++- .../EventCaptureRepositoryImplTest.kt | 100 ++++++++++-------- .../searchTrackEntity/SearchRepositoryTest.kt | 3 + 7 files changed, 89 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index a764b7a894..2e3769bb7b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | Infra: signed APK releases | [simcapture-github-release-signed-apk.yml](.github/workflows/simcapture-github-release-signed-apk.yml) | New file | GitHub Action to create a GitHub Release on a merge to `main` | | Infra: signed APK releases | [app/build.gradle.kts#L85](app/build.gradle.kts#L85) | Code change | App's Package ID set to `com.simprints.simcapture` | | Infra: GitHub Actions filtering | [simcapture-disable-upstream-workflows.yml](.github/workflows/simcapture-disable-upstream-workflows.yml) | New file | GitHub Action to limit allowed Actions to the ones starting with `simcapture-` or `copilot-` | -| Docs: fork-specific README | [README.md#L1-L110](README.md#L1-L110) | Code change | This section in README | +| Docs: fork-specific README | [README.md#L1-L125](README.md#L1-L125) | Code change | This section in README | | Enrolment+ Possible Duplicates | [CustomIntentActivityResultContract.kt#L68](form/src/main/java/org/dhis2/form/ui/customintent/CustomIntentActivityResultContract.kt#L68) | Code addition | Treats non-identify Simprints callouts that return identification matches as `PossibleDuplicates` results | | Enrolment+ Possible Duplicates | [FormView.kt#L654](form/src/main/java/org/dhis2/form/ui/FormView.kt#L654) | Code addition | Stores the returned SID session and hands the enrolment form off to the possible duplicates search flow | | Enrolment+ Possible Duplicates | [EnrollmentActivity.kt#L281](app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt#L281) | Code addition | Launches possible duplicates search from the enrolment form, carrying the biometric field and returned GUID matches | @@ -52,14 +52,14 @@ upstream [dhis2/dhis2-android-capture-app](https://github.com/dhis2/dhis2-androi | Identification+ Enrol Last | [SimprintsRememberCustomIntentFormPresenter.kt](form/src/main/java/org/dhis2/form/simprints/SimprintsRememberCustomIntentFormPresenter.kt) | New file | Builds the Simprints form presenter from Compose field state and placeholder resources | | Identification Confirm Identity | [SearchTEActivity.kt#L128](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt#L128) | Code addition | Registers the confirm-identity SID launcher and waits for Simprints navigation before opening dashboards | | Identification Confirm Identity | [SearchTEIViewModel.kt#L847](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt#L847) | Code addition | Intercepts TEI openings from biometric search to launch Confirm Identity when required | -| Identification Confirm Identity | [SearchTEModule.java#L361](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java#L361) | Code addition | DI for separate Simprints-specific components for Identification Confirm Identity | +| Identification Confirm Identity | [SearchTEModule.java#L374](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java#L374) | Code addition | DI for separate Simprints-specific components for Identification Confirm Identity | | Identification Confirm Identity | [SearchTeiViewModelFactory.kt#L48](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt#L48) | Code addition | Passes Simprints search state into `SearchTEIViewModel` | | Identification Confirm Identity | [SimprintsSearchViewModelFactory.kt](app/src/main/java/org/dhis2/simprints/di/SimprintsSearchViewModelFactory.kt) | New file | Creates activity-scoped Simprints search ViewModel so Confirm Identity and Enrol Last state survives configuration changes | | Identification Confirm Identity | [SimprintsSearchViewModel.kt](app/src/main/java/org/dhis2/simprints/SimprintsSearchViewModel.kt) | New file | Coordinates biometric search handoff for Enrol Last labels and Confirm Identity navigation | | Identification Confirm Identity | [SimprintsResolveConfirmIdentityCalloutUseCase.kt](commons/src/main/java/org/dhis2/commons/simprints/usecases/SimprintsResolveConfirmIdentityCalloutUseCase.kt) | New file | Resolves the selected TEI's biometric GUID and prepares the `CONFIRM_IDENTITY` callout | | ModuleID in Identification | [CustomIntentRepositoryImpl.kt#L105](commonskmm/src/androidMain/kotlin/org/dhis2/mobile/commons/customintents/CustomIntentRepositoryImpl.kt#L105) | Code addition | Overrides `moduleId` for Simprints identification intents with the current user's Org Unit value | | Identification result ordering by score | [SearchTEIViewModel.kt#L715](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt#L715) | Code addition | Routes biometric search loading through SID-order-aware lookup so results follow identification match score order | -| Identification result ordering by score | [SearchTEModule.java#L353](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java#L353) | Code change | DI for separate Simprints-specific components for Identification result ordering by score | +| Identification result ordering by score | [SearchTEModule.java#L366](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java#L366) | Code change | DI for separate Simprints-specific components for Identification result ordering by score | | Identification result ordering by score | [SearchTeiViewModelFactory.kt#L49](app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt#L49) | Code addition | Passes the ordered-result use case into `SearchTEIViewModel` | | Identification result ordering by score | [SimprintsOrderSearchResultsByIdentifyResponseUseCase.kt](commons/src/main/java/org/dhis2/commons/simprints/usecases/SimprintsOrderSearchResultsByIdentifyResponseUseCase.kt) | New file | Orders matching TEIs to follow the SID identification response order | | Identification with less buttons | [ParameterSelectorItemProvider.kt#L92](app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/provider/ParameterSelectorItemProvider.kt#L92) | Code addition | Starts Simprints identification callout immediately on biometric search field click instead of expanding the field | diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt index 95f54659e2..a44fc8f2d3 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt @@ -12,6 +12,7 @@ import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.dhis2.data.dhislogic.DhisEnrollmentUtils import org.dhis2.data.forms.dataentry.SearchTEIRepository import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl @@ -62,7 +63,12 @@ class EventCaptureModule( @Provides @PerActivity - fun provideRepository(d2: D2?): EventCaptureRepository = EventCaptureRepositoryImpl(eventUid, d2) + fun provideRepository(d2: D2): EventCaptureRepository = + EventCaptureRepositoryImpl( + eventUid, + d2, + RampDatastoreRepository(d2), + ) @Provides @PerActivity diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java index 40a1492b71..b8f96853cf 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java @@ -3,8 +3,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.gson.Gson; - import org.dhis2.commons.bindings.SdkExtensionsKt; import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig; import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository; @@ -38,10 +36,10 @@ public class EventCaptureRepositoryImpl implements EventCaptureContract.EventCap private final D2 d2; private final RampDatastoreRepository rampDatastoreRepository; - public EventCaptureRepositoryImpl(String eventUid, D2 d2) { + public EventCaptureRepositoryImpl(String eventUid, D2 d2, RampDatastoreRepository rampDatastoreRepository) { this.eventUid = eventUid; this.d2 = d2; - this.rampDatastoreRepository = new RampDatastoreRepository(d2, new Gson()); + this.rampDatastoreRepository = rampDatastoreRepository; } private Event getCurrentEvent() { 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 dd8b1c8e9d..eee444b164 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -3,7 +3,6 @@ import android.database.sqlite.SQLiteConstraintException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.gson.Gson; import org.dhis2.R; import org.dhis2.bindings.ExtensionsKt; import org.dhis2.bindings.ValueExtensionsKt; @@ -144,7 +143,8 @@ public class SearchRepositoryImpl implements SearchRepository { MetadataIconProvider metadataIconProvider, ProfilePictureProvider profilePictureProvider, DateUtils dateUtils, - CustomIntentRepository customIntentRepository + CustomIntentRepository customIntentRepository, + RampDatastoreRepository rampDatastoreRepository ) { this.teiType = teiType; this.d2 = d2; @@ -168,7 +168,7 @@ public class SearchRepositoryImpl implements SearchRepository { this.metadataIconProvider = metadataIconProvider; this.profilePictureProvider = profilePictureProvider; this.customIntentRepository = customIntentRepository; - this.rampDatastoreRepository = new RampDatastoreRepository(d2, new Gson()); + this.rampDatastoreRepository = rampDatastoreRepository; } @@ -1031,4 +1031,3 @@ private boolean displayOrgUnit() { } - 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 b7e7e4f7bc..614f9ffafc 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; +import com.google.gson.Gson; + import org.dhis2.R; import org.dhis2.commons.data.ProgramConfigurationRepository; import org.dhis2.commons.date.DateLabelProvider; @@ -22,6 +24,7 @@ import org.dhis2.commons.resources.DhisPeriodUtils; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository; import org.dhis2.commons.simprints.repository.SimprintsD2Repository; import org.dhis2.commons.simprints.repository.SimprintsSessionRepository; import org.dhis2.commons.simprints.usecases.SimprintsHasAutoOpenEligibleIdentificationUseCase; @@ -154,6 +157,12 @@ MapTeiEventsToFeatureCollection provideMapTeiEventsToFeatureCollection() { new GetBoundingBox()); } + @Provides + @PerActivity + RampDatastoreRepository provideRampDatastoreRepository(@NonNull D2 d2) { + return new RampDatastoreRepository(d2, new Gson()); + } + @Provides @PerActivity SearchRepository searchRepository(@NonNull D2 d2, @@ -168,7 +177,8 @@ SearchRepository searchRepository(@NonNull D2 d2, ThemeManager themeManager, MetadataIconProvider metadataIconProvider, DateUtils dateUtils, - CustomIntentRepository customIntentRepository) { + CustomIntentRepository customIntentRepository, + RampDatastoreRepository rampDatastoreRepository) { ProfilePictureProvider profilePictureProvider = new ProfilePictureProvider(d2); return new SearchRepositoryImpl(teiType, initialProgram, @@ -185,7 +195,8 @@ SearchRepository searchRepository(@NonNull D2 d2, metadataIconProvider, profilePictureProvider, dateUtils, - customIntentRepository); + customIntentRepository, + rampDatastoreRepository); } @Provides diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImplTest.kt index 6bdd843699..045a7a84e9 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImplTest.kt @@ -2,11 +2,13 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture import io.reactivex.Completable import io.reactivex.Single +import org.dhis2.commons.simprints.ramp.model.ProgramStageHistoryTableConfig +import org.dhis2.commons.simprints.ramp.model.RampDatastoreConfig +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.dhis2.data.dhislogic.AUTH_ALL import org.dhis2.data.dhislogic.AUTH_UNCOMPLETE_EVENT import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.dataelement.DataElement -import org.hisp.dhis.android.core.datastore.DataStoreEntry.builder import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.event.Event @@ -34,6 +36,7 @@ import java.util.GregorianCalendar class EventCaptureRepositoryImplTest { private val eventUid = "eventUid" private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) + private val rampDatastoreRepository: RampDatastoreRepository = mock() private val trackerEventEnrollmentUid = "enrollmentUid" private val testEventStageUid = "stageUid" @@ -67,6 +70,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) assertTrue(repository.isEnrollmentOpen) @@ -85,6 +89,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository.isEnrollmentOpen @@ -110,6 +115,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) assertTrue(!repository.isEnrollmentCancelled) @@ -137,6 +143,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) assertTrue(repository.isEnrollmentCancelled) @@ -151,6 +158,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository.isEventEditable(eventUid) @@ -184,6 +192,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) val testObserver = repository.programStageName().test() @@ -201,6 +210,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -231,6 +241,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -257,6 +268,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) val testStatus = EventStatus.SKIPPED whenever( @@ -279,6 +291,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) val testNewDate = GregorianCalendar(3021, 11, 1).time whenever( @@ -304,6 +317,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository @@ -318,7 +332,11 @@ class EventCaptureRepositoryImplTest { mockEvent() mockSections() - val repository = EventCaptureRepositoryImpl(eventUid, d2) + val repository = EventCaptureRepositoryImpl( + eventUid, + d2, + rampDatastoreRepository, + ) whenever( d2 @@ -337,7 +355,11 @@ class EventCaptureRepositoryImplTest { mockEvent() mockSections() - val repository = EventCaptureRepositoryImpl(eventUid, d2) + val repository = EventCaptureRepositoryImpl( + eventUid, + d2, + rampDatastoreRepository, + ) whenever( d2 @@ -360,6 +382,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository @@ -378,6 +401,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -430,6 +454,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -455,6 +480,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -480,6 +506,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository @@ -498,6 +525,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) repository @@ -516,6 +544,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) val numberOfNotes = 12 whenever( @@ -552,6 +581,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -570,6 +600,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -599,6 +630,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( @@ -622,25 +654,16 @@ class EventCaptureRepositoryImplTest { @Test fun `hasSimprintsRampProgramStageHistoryTable should return true if RAMP program stage history table is configured for event`() { mockEvent() - stubRampConfigRawValue( - """ - { - "programStageHistoryTable": [ - { - "programId": " $testEventProgramUid ", - "followUpVisitProgramStageId": " $testEventStageUid ", - "followUpVisitMaxNumber": 3, - "headerVisitNumberDataElementId": "visitNumberUid" - } - ] - } - """.trimIndent(), + mockRampConfig( + programUid = " $testEventProgramUid ", + programStageUid = " $testEventStageUid ", ) val repository = EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) assertTrue(repository.hasSimprintsRampProgramStageHistoryTable()) @@ -668,25 +691,13 @@ class EventCaptureRepositoryImplTest { .program(testEventProgramUid) .build(), ) - stubRampConfigRawValue( - """ - { - "programStageHistoryTable": [ - { - "programId": "$testEventProgramUid", - "followUpVisitProgramStageId": "$testEventStageUid", - "followUpVisitMaxNumber": 3, - "headerVisitNumberDataElementId": "visitNumberUid" - } - ] - } - """.trimIndent(), - ) + mockRampConfig() val repository = EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) assertFalse(repository.hasSimprintsRampProgramStageHistoryTable()) @@ -702,6 +713,7 @@ class EventCaptureRepositoryImplTest { EventCaptureRepositoryImpl( eventUid, d2, + rampDatastoreRepository, ) whenever( d2 @@ -768,18 +780,22 @@ class EventCaptureRepositoryImplTest { .build() } - private fun stubRampConfigRawValue(value: String) { - whenever( - d2 - .dataStoreModule() - .dataStore() - .value("simprints", "ramp") - .blockingGet(), - ) doReturn builder() - .namespace("simprints") - .key("ramp") - .value(value) - .build() + private fun mockRampConfig( + programUid: String? = testEventProgramUid, + programStageUid: String? = testEventStageUid, + ) { + whenever(rampDatastoreRepository.getConfig()) doReturn + RampDatastoreConfig( + programStageHistoryTables = + listOf( + ProgramStageHistoryTableConfig( + programId = programUid, + followUpVisitProgramStageId = programStageUid, + followUpVisitMaxNumber = 3, + headerVisitNumberDataElementId = "visitNumberUid", + ), + ), + ) } private fun mockEmptySections() { diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt index b2437a4242..4014e2b29b 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt @@ -10,6 +10,7 @@ import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.simprints.ramp.repository.RampDatastoreRepository import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.forms.dataentry.SearchTEIRepository import org.dhis2.data.sorting.SearchSortingValueSetter @@ -98,6 +99,7 @@ class SearchRepositoryTest { private val searchTEIRepository: SearchTEIRepository = mock() private val themeManager: ThemeManager = mock() private val profilePictureProvider: ProfilePictureProvider = mock() + private val rampDatastoreRepository: RampDatastoreRepository = mock() private val dateUtils: DateUtils = DateUtils() @Before @@ -147,6 +149,7 @@ class SearchRepositoryTest { profilePictureProvider, dateUtils, customIntentRepository, + rampDatastoreRepository, ) }