-This is a patch version of the DHIS2 Android App It builds upon the last version including bug fixes that couldn't wait to the next version.
-It includes no functional improvements neither changes in the User Interface. It means that yours users can update without experiencing any change in the UI.
+The new DHIS2 Android App allows offline data capture across all DHIS2 data models. Data and metadata are automatically synchronized whenever there is internet access, always keeping the most relevant data for the logged user in the device.
+The app is compatible and we support 2.38, 2.39, 40. And has no breaking changes with 2.37, 2.36, 2.35 and 2.34.
+## USER EXPERIENCE
-## Bugs fixed
-* [ANDROAPP-5463](https://dhis2.atlassian.net/browse/ANDROAPP-5463) Wrong password in already authenticated account throws invalid url
-* [ANDROAPP-5452](https://dhis2.atlassian.net/browse/ANDROAPP-5452) Notifications not displaying on devices with android 13
-* [ANDROAPP-5426](https://dhis2.atlassian.net/browse/ANDROAPP-5426) DataSet tables not saving text values
-* [ANDROAPP-5425](https://dhis2.atlassian.net/browse/ANDROAPP-5425) App crashing when opening orgUnit field in event creation on android 5 and 6 devices
-* [ANDROAPP-5424](https://dhis2.atlassian.net/browse/ANDROAPP-5424) App not functional in Android 5.0 and 6.0 devices due to expression-parser library
-* [ANDROAPP-5403](https://dhis2.atlassian.net/browse/ANDROAPP-5403) The app displays the keyboard over the selecting menu when the user has multiple apps to use the email or phone number
-* [ANDROAPP-5401](https://dhis2.atlassian.net/browse/ANDROAPP-5401) Infinite loading when applying date filters in tracker program
-* [ANDROAPP-5399](https://dhis2.atlassian.net/browse/ANDROAPP-5399) Improve recomposition on input field in tables
-* [ANDROAPP-5396](https://dhis2.atlassian.net/browse/ANDROAPP-5396) SDK BC: handle d2ErrorCode SERVER_CONNECTION_ERROR
-* [ANDROAPP-5389](https://dhis2.atlassian.net/browse/ANDROAPP-5389) Validation Strategy - Errors
-* [ANDROAPP-5385](https://dhis2.atlassian.net/browse/ANDROAPP-5385) Program dashboard: filter by EventDate includes all EventStatus as side effect
-* [ANDROAPP-5384](https://dhis2.atlassian.net/browse/ANDROAPP-5384) Persist dataset column size when adjusted by the user and revert to default
-* [ANDROAPP-5380](https://dhis2.atlassian.net/browse/ANDROAPP-5380) Sync button crashes app after rotating device in search screen
-* [ANDROAPP-5377](https://dhis2.atlassian.net/browse/ANDROAPP-5377) A TEI enrolled in many programs display to many icons blocking the TEI info card
-* [ANDROAPP-5376](https://dhis2.atlassian.net/browse/ANDROAPP-5376) Login button doesn't work and doesn't present any error
-* [ANDROAPP-5375](https://dhis2.atlassian.net/browse/ANDROAPP-5375) App crashes with some icons
-* [ANDROAPP-5370](https://dhis2.atlassian.net/browse/ANDROAPP-5370) Navigation button does not open in some programs.
-* [ANDROAPP-5369](https://dhis2.atlassian.net/browse/ANDROAPP-5369) NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.replaceAll(java.lang.String, ...
-* [ANDROAPP-5368](https://dhis2.atlassian.net/browse/ANDROAPP-5368) Org Unit value type opens the hierarchy incorrectly
-* [ANDROAPP-5363](https://dhis2.atlassian.net/browse/ANDROAPP-5363) Wrong label displayed while navigating an error or warning
-* [ANDROAPP-5348](https://dhis2.atlassian.net/browse/ANDROAPP-5348) Errors in program rules are not shown after they have been displayed once despite the program rule being reexecuted
-* [ANDROAPP-5343](https://dhis2.atlassian.net/browse/ANDROAPP-5343) Sync flow backwards
-* [ANDROAPP-5342](https://dhis2.atlassian.net/browse/ANDROAPP-5342) Form actionable icons launch action from stored value
-* [ANDROAPP-5340](https://dhis2.atlassian.net/browse/ANDROAPP-5340) Store image and files before value type validation
-* [ANDROAPP-5335](https://dhis2.atlassian.net/browse/ANDROAPP-5335) In TEI dashboard filters appears items related to TEI
-* [ANDROAPP-5334](https://dhis2.atlassian.net/browse/ANDROAPP-5334) "All enrollments" cards show incident date even when not configured
-* [ANDROAPP-5330](https://dhis2.atlassian.net/browse/ANDROAPP-5330) App crash when deleting quantities in "Review" stage
-* [ANDROAPP-5329](https://dhis2.atlassian.net/browse/ANDROAPP-5329) The selected cell is hidden in RTStock program (and datasets) table if the first cell is selected after scroll
-* [ANDROAPP-5328](https://dhis2.atlassian.net/browse/ANDROAPP-5328) Default language not respected (or inconsistent) when changing between servers.
-* [ANDROAPP-5323](https://dhis2.atlassian.net/browse/ANDROAPP-5323) IllegalStateException: Attempting to launch an unregistered ActivityResultLauncher with contract androidx.activity.resul...
-* [ANDROAPP-5255](https://dhis2.atlassian.net/browse/ANDROAPP-5255) [LANDSCAPE] Loading bar never hides in overview screen
-* [ANDROAPP-5253](https://dhis2.atlassian.net/browse/ANDROAPP-5253) Event status filter doesn't remove checkmarks after the reset
-* [ANDROAPP-4710](https://dhis2.atlassian.net/browse/ANDROAPP-4710) Validation Strategy - Mandatory Fields
-* [ANDROAPP-4322](https://dhis2.atlassian.net/browse/ANDROAPP-4322) Analytics legends don't show event's exact date
-* [ANDROAPP-3106](https://dhis2.atlassian.net/browse/ANDROAPP-3106) [Bug]Error when searching with comma char in the values
-* This patch release updates the [Android SDK](https://github.com/dhis2/dhis2-android-sdk) to version 1.8.2.
-
-You can find in Jira details on the [bugs fixed](https://dhis2.atlassian.net/issues/?filter=10461) in this version.
+**Disable referral in tracker programs:** When users add events in a tracker program, the DHIS2 Android Capture app offers three options: Add (for new events), Schedule (for planning future evetns) and Refer (for referrals or transfers). As this third option is not used in many implementations, this new feature enables the admin user to remove that option from the menu to simplify the user experience. The referral option can be hidden using the Android Settings Web App for all programs or for each specific program. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4445) | [Documentation App](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_referrals) | [Documentation Webapp](https://docs.dhis2.org/en/use/android-app/settings-configuration.html#capture_app_android_settings_webapp_appearance_program) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Disable-referrals.png)
+
+**Skip home screen if users only have access to one program:** The home screen of the DHIS2 Android App shows the list of programs and datasets available for the user. The first thing a user must do when using the app is to select the program or dataset to work with. In some implementations, users have access to only one program or dataset. To reduce the number of clicks and streamline the process of data entry, the App will now skip the home screen in the cases where the user has access to only one program or dataset, and will instead open directly to the program or dataset screen with the event, TEI or dataset list. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5148) | [Documentation](https://docs.dhis2.org/en/use/android-app/android-specific-features.html#capture_app_home) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Skip-home-screen.png)
+
+**Display program stage description:** The description for program stage sections was not available to the end user in previous versions of the App. To provide more context and information at the moment of data collection, the description has now been brought to the user interface and will be displayed below the section name. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5151) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_data_entry_form_program_stage_description) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Program-stage-description.png)
+
+**Disable collapsible sections in forms:** Stage sections in the Android App are displayed with collapsible menus that enable the user to open one section at a time. The purpose of this accordion-like implementation is to help the user navigate very long forms. However, some implementations would prefer to list the sections one after the other. This new version of the application enables the admin user to decide if the sections should appear in extended mode. This configuration is made through the Android Settings Web App and will display the sections one after the other with the section name acting as a separator. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5393) | [Documentation App](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_data_entry_form_collapsible_sections) | [Documentation Webapp](https://docs.dhis2.org/en/use/android-app/settings-configuration.html#capture_app_android_settings_webapp_appearance_program) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Not-collapse-sections.png)
+
+**Move working lists under the search bar:** The working lists have been moved from the filters section to the main program screen. In earlier versions, the user had to open the filters to be able to see and select a working list. From this version, the working lists are always visible under the search bar, facilitating their use for filtering out Tracked Entity Instances. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5453) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_working_lists) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Working-list-under-search-bar.png)
+
+**New design for Dataset, Event and TEI cards:** Cards are used for listing datasets, events and TEIs. The new design offers a cleaner and more intuitive layout, replacing the use of colored icons by descriptive text when relevant. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5485) | [Documentation datasets](https://docs.dhis2.org/en/use/android-app/datasets-features.html#capture_app_datsets_cards_design) | [Documentation events](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_cards_design) | [Documentation TEI](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_tei_design) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-New-cards-design.png)
+
+**Implement changes in TEI Dashboard details:** The TEI Dashboard has been redesigned for both portrait and landscape view. The new design offers a cleaner and more intuitive layout, replacing the use of colored icons by text when relevant and moving some secondary actions to the hidden menus. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4019) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-TEI-dashboard.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_tei_design)
+
+**Data entry forms - New inputs per value type:** The inputs for all value types have been redesigned. Tappable areas and texts have been increased and selection modes are improved to offer a cleaner and more intuitive user experience. By default, the Android App will display the previous forms. Admin users are able to opt-in to use the new forms through the Android Settings Web App. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5408) | [Documentation App](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_data_entry_form_new_inputs) | [Documentation Webapp](https://docs.dhis2.org/en/use/android-app/settings-configuration.html#capture_app_android_settings_webapp_appearance_program) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-New-inputs.png)
+
+**[EXPERIMENTAL] TEI Header:** The TEI Header is a title that can be added to the TEI cards and dashboards in the app. The title helps identify a TEI by displaying a summary of key information. It is formed by a concatenation of Tracked Entity Attributes and fixed text. The title is configured through a Program Indicator in the Maintenance app and is assigned to the tracker program in the Android Settings web app. This feature is experimental, and depending on feedback and adoption it will be refined and incorporated in the web Capture app. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5402) | [Documentation App](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_tei_header) | [Documentation Webapp](https://docs.dhis2.org/en/use/android-app/settings-configuration.html#capture_app_android_settings_webapp_appearance_program_specific) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-TEI-Header.png)
+
+**Other improvements for User Experience**
+- Smaller improvements focussing on user experience like a new org unit selector [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4566) | | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-Org-unit-selector.png), or adding a loading spinner during the deletion of big databases. [Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4768) | [Screenshot](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+2.9/release+feature+cards/Android-2-9-loading-when-deleting-data.png)
+
+## MAINTENANCE
+
+**Bug fixing:** You can find the list of bugs fixed [here](https://dhis2.atlassian.net/issues/?filter=10510).
+
+You can find in Jira details on the [new features](https://dhis2.atlassian.net/issues/?filter=10513) in this version.
Remember to check the [documentation](https://www.dhis2.org/android-documentation) for detailed
information of the features included in the App and how to configure DHIS2 to use it.
diff --git a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt
index fc2f2bb63c..f1d8e26584 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt
@@ -2,11 +2,8 @@ package org.dhis2.usescases
import android.content.Context
import android.os.Build
-import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.intent.Intents
-import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import org.dhis2.AppTest
@@ -21,6 +18,7 @@ import org.dhis2.common.keystore.KeyStoreRobot.Companion.USERNAME
import org.dhis2.common.mockwebserver.MockWebServerRobot
import org.dhis2.common.preferences.PreferencesRobot
import org.dhis2.common.rules.DisableAnimations
+import org.dhis2.commons.featureconfig.model.Feature
import org.dhis2.commons.idlingresource.CountingIdlingResourceSingleton
import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton
import org.dhis2.commons.prefs.Preference
@@ -74,6 +72,7 @@ open class BaseTest {
keyStoreRobot = providesKeyStoreRobot(context)
preferencesRobot = providesPreferencesRobot(context)
mockWebServerRobot = providesMockWebserverRobot(context)
+ disableComposeForms()
}
}
@@ -160,6 +159,10 @@ open class BaseTest {
(context.applicationContext as AppTest).deleteDatabase(DB_TO_IMPORT)
}
+ private fun disableComposeForms() {
+ preferencesRobot.saveValue(Feature.COMPOSE_FORMS.name, false)
+ }
+
companion object {
@ClassRule
@JvmField
diff --git a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt
index fa4cd4e771..17bd65dd4d 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt
@@ -1,18 +1,14 @@
package org.dhis2.usescases.datasets
-import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.onNodeWithTag
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.dhis2.R
import org.dhis2.common.BaseRobot
import org.dhis2.utils.customviews.DateViewHolder
-import org.hamcrest.CoreMatchers.allOf
fun dataSetInitialRobot(dataSetInitialRobot: DataSetInitialRobot.() -> Unit) {
diff --git a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt
index f0ed9ee295..659b9d2bd8 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt
@@ -48,7 +48,7 @@ class DataSetTest : BaseTest() {
@Test
fun shouldCreateNewDataSet() {
- val period = "Dec 2022"
+ val period = "Oct 2023"
val orgUnit = "Ngelehun CHC"
startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail)
@@ -57,7 +57,7 @@ class DataSetTest : BaseTest() {
}
dataSetInitialRobot {
clickOnInputOrgUnit()
- orgUnitSelectorRobot(composeTestRule){
+ orgUnitSelectorRobot(composeTestRule) {
selectTreeOrgUnit(orgUnit)
}
clickOnInputPeriod()
diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt
index 5df4772583..56cb29fa1d 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt
@@ -1,10 +1,11 @@
package org.dhis2.usescases.event
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.onNodeWithText
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
@@ -13,7 +14,6 @@ import org.dhis2.common.BaseRobot
import org.dhis2.common.matchers.hasCompletedPercentage
import org.dhis2.usescases.event.entity.EventDetailsUIModel
import org.hamcrest.CoreMatchers.allOf
-import org.hamcrest.CoreMatchers.not
fun eventRegistrationRobot(eventRegistrationRobot: EventRegistrationRobot.() -> Unit) {
EventRegistrationRobot().apply {
@@ -48,25 +48,12 @@ class EventRegistrationRobot : BaseRobot() {
onView(withId(R.id.navigation_details)).perform(click())
}
- fun checkEventDetails(eventDetails: EventDetailsUIModel) {
+ fun checkEventDetails(eventDetails: EventDetailsUIModel, composeTestRule: ComposeTestRule) {
onView(withId(R.id.detailsStageName)).check(matches(withText(eventDetails.programStage)))
onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(eventDetails.completedPercentage)))
- onView(withId(R.id.date_layout)).check(
- matches(
- allOf(
- isEnabled(),
- hasDescendant(allOf(withId(R.id.date), withText(eventDetails.eventDate)))
- )
- )
- )
- onView(withId(R.id.org_unit_layout)).check(
- matches(
- allOf(
- not(isEnabled()),
- hasDescendant(allOf(withId(R.id.org_unit), withText(eventDetails.orgUnit)))
- )
- )
- )
+
+ composeTestRule.onNodeWithText(formatStoredDateToUI(eventDetails.eventDate)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(eventDetails.orgUnit).assertIsDisplayed()
}
fun clickOnShare() {
@@ -102,4 +89,22 @@ class EventRegistrationRobot : BaseRobot() {
fun clickNextButton() {
waitForView(withId(R.id.action_button)).perform(click())
}
+
+ private fun formatStoredDateToUI(dateValue: String): String {
+ val components = dateValue.split("/")
+
+ val year = components[2]
+ val month = if (components[1].length == 1) {
+ "0${components[1]}"
+ } else {
+ components[1]
+ }
+ val day = if (components[0].length == 1) {
+ "0${components[0]}"
+ } else {
+ components[0]
+ }
+
+ return "$day/$month/$year"
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt
index 67b8161f46..02c8bc8ba3 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt
@@ -11,7 +11,6 @@ import org.dhis2.usescases.event.entity.ProgramStageUIModel
import org.dhis2.usescases.event.entity.TEIProgramStagesUIModel
import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity
import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity
-import org.dhis2.usescases.form.formRobot
import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity
import org.dhis2.usescases.programEventDetail.eventList.EventListFragment
import org.dhis2.usescases.programevent.robot.programEventsRobot
@@ -74,7 +73,7 @@ class EventTest: BaseTest() {
eventRegistrationRobot {
checkEventFormDetails(eventDetails)
clickOnDetails()
- checkEventDetails(eventDetails)
+ checkEventDetails(eventDetails, composeTestRule)
}
}
diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt
index b49f96b27c..0f0ef7320a 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowRobot.kt
@@ -93,14 +93,14 @@ class TeiFlowRobot : BaseRobot() {
}
}
- fun changeDueDate(date: DateRegistrationUIModel, programStage: String, orgUnit: String) {
+ fun changeDueDate(date: DateRegistrationUIModel, programStage: String, orgUnit: String, composeTestRule: ComposeTestRule) {
teiDashboardRobot {
clickOnStageGroup(programStage)
clickOnEventGroupByStageUsingOU(orgUnit)
}
eventRobot {
- clickOnEventDueDate()
+ clickOnEventDueDate(composeTestRule)
selectSpecificDate(date.year, date.month, date.day)
acceptUpdateEventDate()
}
diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt
index 428944489a..6ec2122d91 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt
@@ -11,7 +11,6 @@ import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity
import org.dhis2.usescases.programEventDetail.eventList.EventListFragment
import org.dhis2.usescases.programevent.robot.programEventsRobot
import org.dhis2.usescases.teidashboard.robot.eventRobot
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -30,7 +29,6 @@ class ProgramEventTest : BaseTest() {
return arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
- @Ignore("Nondeterministic")
@Test
fun shouldCreateNewEventAndCompleteIt() {
val eventOrgUnit = "Ngelehun CHC"
@@ -124,7 +122,7 @@ class ProgramEventTest : BaseTest() {
}
eventRobot {
clickOnDetails()
- checkEventDetails(eventDate, eventOrgUnit)
+ checkEventDetails(eventDate, eventOrgUnit, composeTestRule)
}
}
diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt
index 3f605485fb..b7cf9e5d5f 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt
@@ -1,6 +1,8 @@
package org.dhis2.usescases.searchte
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -9,12 +11,12 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
-import com.mapbox.mapboxsdk.maps.MapboxMap
import dispatch.android.espresso.IdlingDispatcherProvider
import dispatch.android.espresso.IdlingDispatcherProviderRule
import org.dhis2.R
import org.dhis2.bindings.app
import org.dhis2.common.idlingresources.MapIdlingResource
+import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG
import org.dhis2.usescases.BaseTest
import org.dhis2.usescases.flow.teiFlow.TeiFlowTest
import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel
@@ -40,7 +42,6 @@ class SearchTETest : BaseTest() {
val rule = ActivityTestRule(SearchTEActivity::class.java, false, false)
private var mapIdlingResource: MapIdlingResource? = null
- private var map: MapboxMap? = null
private val customDispatcherProvider =
context.applicationContext.app().appComponent().customDispatcherProvider()
@@ -58,7 +59,6 @@ class SearchTETest : BaseTest() {
fun shouldSuccessfullySearchByName() {
val firstName = "Tim"
val firstNamePosition = 0
- val filterCount = "1"
val orgUnit = "Ngelehun CHC"
prepareChildProgrammeIntentAndLaunchActivity(rule)
@@ -92,7 +92,6 @@ class SearchTETest : BaseTest() {
val firstNamePosition = 0
val lastName = "Jones"
val lastNamePosition = 1
- val filterCount = "2"
prepareChildProgrammeIntentAndLaunchActivity(rule)
@@ -125,7 +124,6 @@ class SearchTETest : BaseTest() {
val displayInListData = createDisplayListFields()
val namePosition = 0
val lastNamePosition = 1
- val filterCount = "3"
setDatePicker()
prepareTestAdultWomanProgrammeIntentAndLaunchActivity(rule)
@@ -179,12 +177,13 @@ class SearchTETest : BaseTest() {
teiFlowRobot {
registerTEI(registerTeiDetails)
- changeDueDate(overdueDate, programStage, orgUnit)
+ changeDueDate(overdueDate, programStage, orgUnit, composeTestRule)
+ pressBack()
+ composeTestRule.onNodeWithTag(SECONDARY_BUTTON_TAG).performClick()
pressBack()
}
filterRobot {
- waitToDebounce(5000)
clickOnFilter()
clickOnFilterBy(eventStatusFilter)
clickOnFilterOverdueOption()
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 a5f8a0a7cc..90370f5491 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt
@@ -16,7 +16,6 @@ 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.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -145,7 +144,10 @@ class TeiDashboardTest : BaseTest() {
clickOnFab()
clickOnReferral()
clickOnFirstReferralEvent()
- clickOnReferralOption()
+ clickOnReferralOption(
+ composeTestRule,
+ context.getString(R.string.one_time)
+ )
clickOnReferralNextButton()
checkEventWasCreated(LAB_MONITORING)
}
diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt
index c6b882a480..625f39c53a 100644
--- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt
+++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt
@@ -1,15 +1,15 @@
package org.dhis2.usescases.teidashboard.robot
+import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
-import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
-import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
@@ -25,7 +25,6 @@ import org.dhis2.ui.dialogs.bottomsheet.MAIN_BUTTON_TAG
import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG
import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder
import org.hamcrest.CoreMatchers.allOf
-import org.hamcrest.CoreMatchers.not
fun eventRobot(eventRobot: EventRobot.() -> Unit) {
EventRobot().apply {
@@ -134,17 +133,37 @@ class EventRobot : BaseRobot() {
onView(withId(R.id.possitive)).perform(click())
}
- fun clickOnEventDueDate() {
- onView(withId(R.id.due_date)).perform(click())
+ fun clickOnEventDueDate(composeTestRule: ComposeTestRule) {
+ composeTestRule.onNodeWithTag("INPUT_DATE_TIME_TEXT").assertIsDisplayed()
+ composeTestRule.onNodeWithTag("INPUT_DATE_TIME_TEXT").performClick()
+
}
fun selectSpecificDate(year: Int, monthOfYear: Int, dayOfMonth: Int) {
onView(withId(R.id.datePicker)).perform(PickerActions.setDate(year, monthOfYear, dayOfMonth))
}
- fun checkEventDetails(eventDate: String, eventOrgUnit: String) {
+ fun checkEventDetails(eventDate: String, eventOrgUnit: String, composeTestRule: ComposeTestRule) {
onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(100)))
- onView(withId(R.id.date_layout)).check(matches(allOf(isEnabled(),hasDescendant(allOf(withId(R.id.date), withText(eventDate))))))
- onView(withId(R.id.org_unit_layout)).check(matches(allOf(not(isEnabled()), hasDescendant(allOf(withId(R.id.org_unit), withText(eventOrgUnit))))))
+ composeTestRule.onNodeWithText(formatStoredDateToUI(eventDate)).assertIsDisplayed()
+ composeTestRule.onNodeWithText(eventOrgUnit).assertIsDisplayed()
+ }
+
+ private fun formatStoredDateToUI(dateValue: String): String {
+ val components = dateValue.split("/")
+
+ val year = components[2]
+ val month = if (components[1].length == 1) {
+ "0${components[1]}"
+ } else {
+ components[1]
+ }
+ val day = if (components[0].length == 1) {
+ "0${components[0]}"
+ } else {
+ components[0]
+ }
+
+ return "$day/$month/$year"
}
}
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 ac270d0eed..3c0f598942 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
@@ -2,10 +2,10 @@ package org.dhis2.usescases.teidashboard.robot
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -151,14 +151,13 @@ class TeiDashboardRobot : BaseRobot() {
.perform(actionOnItemAtPosition(0, click()))
}
- fun clickOnReferralOption() {
- onView(withId(R.id.one_time)).perform(click())
+ fun clickOnReferralOption(composeTestRule: ComposeTestRule, oneTime: String) {
+ composeTestRule.onNodeWithText(oneTime).performClick()
}
fun clickOnReferralNextButton() {
waitForView(withId(R.id.action_button)).perform(click())
}
-
fun checkEventWasCreated(eventName: String) {
onView(withId(R.id.tei_recycler))
diff --git a/app/src/dhisPlayServices/java/org/dhis2/usescases/main/program/ProgramAnimation.kt b/app/src/dhisPlayServices/java/org/dhis2/usescases/main/program/ProgramAnimation.kt
index cc51bf6f12..fb722c430e 100644
--- a/app/src/dhisPlayServices/java/org/dhis2/usescases/main/program/ProgramAnimation.kt
+++ b/app/src/dhisPlayServices/java/org/dhis2/usescases/main/program/ProgramAnimation.kt
@@ -3,7 +3,7 @@ package org.dhis2.usescases.main.program
import android.animation.ValueAnimator
import android.graphics.drawable.GradientDrawable
import android.view.animation.OvershootInterpolator
-import org.dhis2.Bindings.dp
+import org.dhis2.bindings.dp
class ProgramAnimation {
diff --git a/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt
index 06688cac7a..6ed0cf8816 100644
--- a/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt
+++ b/app/src/dhisPlayServices/java/org/dhis2/utils/granularsync/GranularSyncModule.kt
@@ -30,6 +30,7 @@ import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Dispatchers
import org.dhis2.commons.prefs.PreferenceProvider
+import org.dhis2.commons.resources.ColorUtils
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.commons.schedulers.SchedulerProvider
import org.dhis2.commons.sync.SyncContext
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8d8b9ebd04..786c96c1fd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,7 @@
+
+
+
eventDate()!!
- EventStatus.COMPLETED -> eventDate()!!
- EventStatus.SCHEDULE -> dueDate()!!
- EventStatus.SKIPPED -> dueDate()!!
- EventStatus.VISITED -> eventDate()!!
- EventStatus.OVERDUE -> dueDate()!!
- null -> Date()
- }
-}
-
-fun EventCollectionRepository.applyFilters(
- periodFilters: MutableList,
- orgUnitFilters: MutableList,
- stateFilters: MutableList,
- assignedUser: String?,
- eventStatusFilters: MutableList,
- catOptComboFilters: MutableList,
-): EventCollectionRepository {
- var eventRepo = this
-
- if (periodFilters.isNotEmpty()) {
- eventRepo = eventRepo.byEventDate().inDatePeriods(periodFilters)
- }
- if (orgUnitFilters.isNotEmpty()) {
- eventRepo = eventRepo.byOrganisationUnitUid().`in`(orgUnitFilters)
- }
- if (catOptComboFilters.isNotEmpty()) {
- eventRepo = eventRepo.byAttributeOptionComboUid()
- .`in`(UidsHelper.getUids(catOptComboFilters))
- }
- if (eventStatusFilters.isNotEmpty()) {
- eventRepo = eventRepo.byStatus().`in`(eventStatusFilters)
- }
- if (stateFilters.isNotEmpty()) {
- eventRepo = eventRepo.byAggregatedSyncState().`in`(stateFilters)
- }
- if (assignedUser != null) {
- eventRepo = eventRepo.byAssignedUser().eq(assignedUser)
- }
-
- return eventRepo
-}
diff --git a/app/src/main/java/org/dhis2/data/service/SyncDataWorker.java b/app/src/main/java/org/dhis2/data/service/SyncDataWorker.java
index fb473a701e..d1c6b97306 100644
--- a/app/src/main/java/org/dhis2/data/service/SyncDataWorker.java
+++ b/app/src/main/java/org/dhis2/data/service/SyncDataWorker.java
@@ -5,6 +5,7 @@
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
+import android.content.pm.ServiceInfo;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -176,7 +177,11 @@ private void triggerNotification(String title, String content, int progress) {
.setProgress(100, progress, false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
- setForegroundAsync(new ForegroundInfo(SyncDataWorker.SYNC_DATA_ID, notificationBuilder.build()));
+ setForegroundAsync(new ForegroundInfo(
+ SyncDataWorker.SYNC_DATA_ID,
+ notificationBuilder.build(),
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC : 0
+ ));
}
diff --git a/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java b/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java
index 5677d602c7..b0b066eb22 100644
--- a/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java
+++ b/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java
@@ -7,6 +7,7 @@
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
+import android.content.pm.ServiceInfo;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -192,7 +193,11 @@ private void triggerNotification(String title, String content, int progress) {
.setProgress(100, progress, false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
- setForegroundAsync(new ForegroundInfo(SyncMetadataWorker.SYNC_METADATA_ID, notificationBuilder.build()));
+ setForegroundAsync(new ForegroundInfo(
+ SyncMetadataWorker.SYNC_METADATA_ID,
+ notificationBuilder.build(),
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC : 0
+ ));
}
private void cancelNotification() {
diff --git a/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt b/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt
index c349446a43..712b04f075 100644
--- a/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt
+++ b/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt
@@ -8,7 +8,7 @@ import timber.log.Timber
class SyncStatusController {
private var progressStatusMap: Map = emptyMap()
- private val downloadStatus = MutableLiveData(SyncStatusData(false))
+ private val downloadStatus = MutableLiveData(SyncStatusData(isInitialSync = true))
fun observeDownloadProcess(): LiveData = downloadStatus
@@ -98,4 +98,8 @@ class SyncStatusController {
SyncStatusData(true, true, progressStatusMap),
)
}
+
+ fun restore() {
+ downloadStatus.postValue(SyncStatusData())
+ }
}
diff --git a/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt b/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt
index 9710fe34bb..245d514153 100644
--- a/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt
+++ b/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt
@@ -4,9 +4,10 @@ import org.hisp.dhis.android.core.arch.call.D2ProgressStatus
import org.hisp.dhis.android.core.arch.call.D2ProgressSyncStatus
data class SyncStatusData(
- val running: Boolean,
+ val running: Boolean? = null,
val downloadingMedia: Boolean = false,
val programSyncStatusMap: Map = emptyMap(),
+ val isInitialSync: Boolean = false,
) {
fun isProgramDownloading(uid: String): Boolean {
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt
index a0ed56a271..73c2066e5a 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt
+++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt
@@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting
import io.reactivex.Flowable
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -209,7 +210,10 @@ class DataValuePresenter(
}
fun onSaveValueChange(cell: TableCell) {
- launch(dispatcherProvider.io()) {
+ launch(
+ dispatcherProvider.io(),
+ start = CoroutineStart.ATOMIC,
+ ) {
saveValue(cell)
view.onValueProcessed()
}
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModel.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModel.java
index df8433e4c4..d97501be74 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModel.java
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailModel.java
@@ -12,6 +12,10 @@
@AutoValue
public abstract class DataSetDetailModel {
+
+ @NonNull
+ public abstract String datasetUid();
+
@NonNull
public abstract String orgUnitUid();
@@ -49,7 +53,7 @@ public abstract class DataSetDetailModel {
public abstract String nameCategoryOptionCombo();
@NonNull
- public static DataSetDetailModel create(@NonNull String orgUnitUid, @NonNull String catOptionComboUid, @NonNull String periodId, @NonNull String orgUnitName, String nameCatCombo, String namePeriod, State state, String periodType, Boolean displayOrgUnitName, Boolean isComplete, Date lastUpdated, String nameCategoryOptionCombo) {
- return new AutoValue_DataSetDetailModel(orgUnitUid, catOptionComboUid, periodId, orgUnitName, nameCatCombo, namePeriod, state, periodType, displayOrgUnitName, isComplete, lastUpdated, nameCategoryOptionCombo);
+ public static DataSetDetailModel create(@NonNull String datasetUid, @NonNull String orgUnitUid, @NonNull String catOptionComboUid, @NonNull String periodId, @NonNull String orgUnitName, String nameCatCombo, String namePeriod, State state, String periodType, Boolean displayOrgUnitName, Boolean isComplete, Date lastUpdated, String nameCategoryOptionCombo) {
+ return new AutoValue_DataSetDetailModel(datasetUid, orgUnitUid, catOptionComboUid, periodId, orgUnitName, nameCatCombo, namePeriod, state, periodType, displayOrgUnitName, isComplete, lastUpdated, nameCategoryOptionCombo);
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepository.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepository.java
index c6ce7e3add..ea56821bd7 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepository.java
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepository.java
@@ -19,4 +19,11 @@ public interface DataSetDetailRepository {
CategoryOptionCombo getCatOptCombo(String selectedCatOptionCombo);
boolean dataSetHasAnalytics();
+
+ boolean dataSetIsEditable(
+ String datasetUid,
+ String periodId,
+ String organisationUnitUid,
+ String attributeOptionComboUid
+ );
}
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java
index 55d21e10c8..8a58a9d8c3 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailRepositoryImpl.java
@@ -11,7 +11,9 @@
import org.hisp.dhis.android.core.category.CategoryOptionCombo;
import org.hisp.dhis.android.core.common.State;
import org.hisp.dhis.android.core.dataset.DataSetCompleteRegistration;
+import org.hisp.dhis.android.core.dataset.DataSetEditableStatus;
import org.hisp.dhis.android.core.dataset.DataSetInstanceCollectionRepository;
+import org.hisp.dhis.android.core.event.EventEditableStatus;
import org.hisp.dhis.android.core.organisationunit.OrganisationUnit;
import org.hisp.dhis.android.core.period.DatePeriod;
import org.hisp.dhis.android.core.period.Period;
@@ -83,6 +85,7 @@ public Flowable> dataSetGroups(List orgUnits, L
//"Category Combination Name" + "Category option selected"
return DataSetDetailModel.create(
+ dataSetReport.dataSetUid(),
dataSetReport.organisationUnitUid(),
dataSetReport.attributeOptionComboUid(), //catComboUid
dataSetReport.period(),
@@ -166,6 +169,22 @@ public boolean dataSetHasAnalytics() {
}
+ @Override
+ public boolean dataSetIsEditable(
+ String datasetUid,
+ String periodId,
+ String organisationUnitUid,
+ String attributeOptionComboUid) {
+ return d2.dataSetModule()
+ .dataSetInstanceService()
+ .blockingGetEditableStatus(
+ datasetUid,
+ periodId,
+ organisationUnitUid,
+ attributeOptionComboUid
+ ) instanceof DataSetEditableStatus.Editable;
+ }
+
private CategoryCombo getCategoryComboFromOptionCombo(String categoryOptionComboUid) {
CategoryOptionCombo catOptionCombo = d2.categoryModule()
.categoryOptionCombos()
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt
index 7a05a097ab..ba611acf5e 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListAdapter.kt
@@ -34,6 +34,12 @@ class DataSetListAdapter(
composeView.setContent {
val card = cardMapper.map(
dataset = it,
+ editable = viewModel.isEditable(
+ datasetUid = it.datasetUid(),
+ periodId = it.periodId(),
+ organisationUnitUid = it.orgUnitUid(),
+ attributeOptionComboUid = it.catOptionComboUid(),
+ ),
onSyncIconClick = {
viewModel.syncDataSet(it)
},
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt
index ee42a60763..4cb863670c 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModel.kt
@@ -16,7 +16,7 @@ import org.dhis2.utils.Action
import timber.log.Timber
class DataSetListViewModel(
- dataSetDetailRepository: DataSetDetailRepository,
+ val dataSetDetailRepository: DataSetDetailRepository,
schedulerProvider: SchedulerProvider,
val filterManager: FilterManager,
val matomoAnalyticsController: MatomoAnalyticsController,
@@ -74,4 +74,18 @@ class DataSetListViewModel(
fun updateData() {
filterManager.publishData()
}
+
+ fun isEditable(
+ datasetUid: String,
+ periodId: String,
+ organisationUnitUid: String,
+ attributeOptionComboUid: String,
+ ): Boolean {
+ return dataSetDetailRepository.dataSetIsEditable(
+ datasetUid,
+ periodId,
+ organisationUnitUid,
+ attributeOptionComboUid,
+ )
+ }
}
diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt
index 3777b593a0..0a21fb02e6 100644
--- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt
+++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.SyncDisabled
import androidx.compose.material.icons.outlined.SyncProblem
+import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.dhis2.R
@@ -30,13 +31,14 @@ class DatasetCardMapper(
fun map(
dataset: DataSetDetailModel,
+ editable: Boolean,
onSyncIconClick: () -> Unit,
onCardCLick: () -> Unit,
): ListCardUiModel {
return ListCardUiModel(
title = dataset.namePeriod(),
lastUpdated = dataset.lastUpdated().toDateSpan(context),
- additionalInfo = getAdditionalInfoList(dataset),
+ additionalInfo = getAdditionalInfoList(dataset, editable),
actionButton = {
ProvideSyncButton(
state = dataset.state(),
@@ -51,6 +53,7 @@ class DatasetCardMapper(
private fun getAdditionalInfoList(
dataset: DataSetDetailModel,
+ editable: Boolean,
): List {
val list = mutableListOf()
@@ -74,11 +77,36 @@ class DatasetCardMapper(
state = dataset.state(),
)
- // checkViewOnly()
+ checkViewOnly(
+ list = list,
+ editable = editable,
+ )
return list
}
+ private fun checkViewOnly(
+ list: MutableList,
+ editable: Boolean,
+ ) {
+ if (!editable) {
+ list.add(
+ AdditionalInfoItem(
+ icon = {
+ Icon(
+ imageVector = Icons.Outlined.Visibility,
+ contentDescription = resourceManager.getString(R.string.view_only),
+ tint = AdditionalInfoItemColor.DISABLED.color,
+ )
+ },
+ value = resourceManager.getString(R.string.view_only),
+ isConstantItem = true,
+ color = AdditionalInfoItemColor.DISABLED.color,
+ ),
+ )
+ }
+ }
+
private fun checkCategoryCombination(
list: MutableList,
dataset: DataSetDetailModel,
diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt
index a3e5be3493..1a68faac56 100644
--- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt
+++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt
@@ -5,6 +5,12 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.DatePicker
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.dp
import androidx.databinding.DataBindingUtil
import org.dhis2.App
import org.dhis2.R
@@ -13,6 +19,8 @@ import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker
import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener
import org.dhis2.databinding.ActivityEventScheduledBinding
import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate
import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity
import org.dhis2.usescases.general.ActivityGlobalAbstract
import org.dhis2.utils.DateUtils
@@ -76,14 +84,13 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract.
override fun setEvent(event: Event) {
this.event = event
- binding.dueDate.setText(DateUtils.uiDateFormat().format(event.dueDate()))
-
when (event.status()) {
EventStatus.OVERDUE, EventStatus.SCHEDULE -> {
binding.actionButton.visibility = View.VISIBLE
binding.actionButton.text = getString(R.string.skip)
binding.actionButton.setOnClickListener { presenter.skipEvent() }
}
+
else -> {
binding.actionButton.visibility = View.GONE
binding.actionButton.setOnClickListener(null)
@@ -91,18 +98,46 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract.
}
}
- override fun setStage(programStage: ProgramStage) {
+ override fun setStage(programStage: ProgramStage, event: Event) {
this.stage = programStage
binding.programStage = programStage
- binding.dateLayout.hint =
- programStage.executionDateLabel() ?: getString(R.string.report_date)
- binding.dueDateLayout.hint = programStage.dueDateLabel() ?: getString(R.string.due_date)
- if (programStage.hideDueDate() == true) {
- binding.dueDateLayout.visibility = View.GONE
- }
+ binding.scheduledEventFieldContainer.apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ Column {
+ val eventDate = EventDate(
+ label = programStage.executionDateLabel()
+ ?: getString(R.string.report_date),
+ dateValue = "",
+ )
+ Spacer(modifier = Modifier.height(16.dp))
- setEventDateClickListener(programStage.periodType())
+ ProvideInputDate(
+ eventDate = eventDate,
+ allowsManualInput = false,
+ detailsEnabled = true,
+ onDateClick = { setEvenDateListener(programStage.periodType()) },
+ onDateSet = {},
+ onClear = {},
+ )
+ if (programStage.hideDueDate() == false) {
+ val dueDate = EventDate(
+ label = programStage.dueDateLabel() ?: getString(R.string.due_date),
+ dateValue = DateUtils.uiDateFormat().format(event.dueDate() ?: ""),
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideInputDate(
+ eventDate = dueDate,
+ detailsEnabled = true,
+ onDateClick = { setDueDateListener(programStage.periodType()) },
+ onDateSet = {},
+ onClear = {},
+ )
+ }
+ }
+ }
+ }
}
override fun setProgram(program: Program) {
@@ -110,63 +145,61 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract.
binding.name = program.displayName()
}
- fun setEventDateClickListener(periodType: PeriodType?) {
- binding.date.setOnClickListener {
- if (periodType == null) {
- showCustomCalendar(false)
- } else {
- var minDate =
- DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType)
- val lastPeriodDate =
- DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true)
-
- if (lastPeriodDate.after(
- DateUtils.getInstance().getNextPeriod(
- program.expiryPeriodType(),
- minDate,
- 0,
- ),
- )
- ) {
- minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0)
- }
+ private fun setEvenDateListener(periodType: PeriodType?) {
+ if (periodType == null) {
+ showCustomCalendar(false)
+ } else {
+ var minDate =
+ DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType)
+ val lastPeriodDate =
+ DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true)
- PeriodDialog()
- .setPeriod(periodType)
- .setMinDate(minDate)
- .setMaxDate(DateUtils.getInstance().today)
- .setPossitiveListener { selectedDate -> presenter.setEventDate(selectedDate) }
- .show(supportFragmentManager, PeriodDialog::class.java.simpleName)
+ if (lastPeriodDate.after(
+ DateUtils.getInstance().getNextPeriod(
+ program.expiryPeriodType(),
+ minDate,
+ 0,
+ ),
+ )
+ ) {
+ minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0)
}
+
+ PeriodDialog()
+ .setPeriod(periodType)
+ .setMinDate(minDate)
+ .setMaxDate(DateUtils.getInstance().today)
+ .setPossitiveListener { selectedDate -> presenter.setEventDate(selectedDate) }
+ .show(supportFragmentManager, PeriodDialog::class.java.simpleName)
}
+ }
- binding.dueDate.setOnClickListener {
- if (periodType == null) {
- showCustomCalendar(true)
- } else {
- var minDate =
- DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType)
- val lastPeriodDate =
- DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true)
-
- if (lastPeriodDate.after(
- DateUtils.getInstance().getNextPeriod(
- program.expiryPeriodType(),
- minDate,
- 0,
- ),
- )
- ) {
- minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0)
- }
+ private fun setDueDateListener(periodType: PeriodType?) {
+ if (periodType == null) {
+ showCustomCalendar(true)
+ } else {
+ var minDate =
+ DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType)
+ val lastPeriodDate =
+ DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true)
- PeriodDialog()
- .setPeriod(periodType)
- .setMinDate(minDate)
- .setMaxDate(DateUtils.getInstance().today)
- .setPossitiveListener { selectedDate -> presenter.setDueDate(selectedDate) }
- .show(supportFragmentManager, PeriodDialog::class.java.simpleName)
+ if (lastPeriodDate.after(
+ DateUtils.getInstance().getNextPeriod(
+ program.expiryPeriodType(),
+ minDate,
+ 0,
+ ),
+ )
+ ) {
+ minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0)
}
+
+ PeriodDialog()
+ .setPeriod(periodType)
+ .setMinDate(minDate)
+ .setMaxDate(DateUtils.getInstance().today)
+ .setPossitiveListener { selectedDate -> presenter.setDueDate(selectedDate) }
+ .show(supportFragmentManager, PeriodDialog::class.java.simpleName)
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt
index 387c59d5fd..0bf7b898a4 100644
--- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt
+++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt
@@ -12,7 +12,7 @@ class ScheduledEventContract {
interface View : AbstractActivityContracts.View {
fun setEvent(event: Event)
- fun setStage(programStage: ProgramStage)
+ fun setStage(programStage: ProgramStage, event: Event)
fun setProgram(program: Program)
fun openInitialActivity()
fun openFormActivity()
diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt
index 091c67249f..407a2991c5 100644
--- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt
+++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt
@@ -39,7 +39,7 @@ class ScheduledEventPresenterImpl(
{ stageProgramEventData ->
val (stage, program, event) = stageProgramEventData
view.setProgram(program)
- view.setStage(stage)
+ view.setStage(stage, event)
view.setEvent(event)
},
{ Timber.e(it) },
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt
index bcbd0bb3b2..7388e7890c 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt
@@ -188,7 +188,7 @@ class EventDetailsRepository(
return d2.categoryModule().categoryOptionCombos()
.byCategoryComboUid().eq(categoryComboUid)
.byCategoryOptions(categoryOptionsUid)
- .one()?.blockingGet()?.uid()
+ .one().blockingGet()?.uid()
}
fun getCatOption(selectedOption: String?): CategoryOption? {
@@ -207,7 +207,7 @@ class EventDetailsRepository(
.categoryOptions()
.withOrganisationUnits()
.byCategoryUid(categoryUid)
- .blockingGet() ?: emptyList()
+ .blockingGet()
}
fun getOptionsFromCatOptionCombo(): Map? {
@@ -277,7 +277,9 @@ class EventDetailsRepository(
FeatureType.POLYGON,
FeatureType.MULTI_POLYGON,
-> eventRepository.setGeometry(geometry)
+
else -> {
+ // no-op
}
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt
index 2923d6cd88..f66d01c6a9 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt
@@ -16,16 +16,18 @@ class ConfigureEventCatCombo(
private var selectedCategoryOptions = mapOf()
operator fun invoke(categoryOption: Pair? = null): Flow {
- categoryOption?.let {
- updateSelectedOptions(it)
- }
repository.catCombo().apply {
+ val categories = getCategories(this?.categories())
+ val categoryOptions = getCategoryOptions()
+
+ updateSelectedOptions(categoryOption, categories, categoryOptions)
+
return flowOf(
EventCatCombo(
uid = getCatComboUid(this?.uid() ?: "", this?.isDefault ?: false),
isDefault = this?.isDefault ?: false,
- categories = getCategories(this?.categories()),
- categoryOptions = getCategoryOptions(),
+ categories = categories,
+ categoryOptions = categoryOptions,
selectedCategoryOptions = selectedCategoryOptions,
isCompleted = isCompleted(
isDefault = this?.isDefault ?: true,
@@ -73,14 +75,27 @@ class ConfigureEventCatCombo(
private fun updateSelectedOptions(
categoryOption: Pair?,
+ categories: List,
+ categoryOptions: Map?,
): Map {
- categoryOption?.let { pair ->
- val copy = selectedCategoryOptions.toMutableMap()
- copy[pair.first] = pair.second?.let { categoryOptionId ->
- repository.getCatOption(categoryOptionId)
+ if (categoryOption == null) {
+ categories.forEach { category ->
+ categoryOptions?.get(category.uid)?.let { categoryOption ->
+ val copy = selectedCategoryOptions.toMutableMap()
+ copy[category.uid] = categoryOption
+ selectedCategoryOptions = copy
+ }
+ }
+ } else {
+ categoryOption.let { pair ->
+ val copy = selectedCategoryOptions.toMutableMap()
+ copy[pair.first] = pair.second?.let { categoryOptionId ->
+ repository.getCatOption(categoryOptionId)
+ }
+ selectedCategoryOptions = copy
}
- selectedCategoryOptions = copy
}
+
return selectedCategoryOptions
}
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt
index 9f3e4f2b86..e2ad0490f2 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt
@@ -69,10 +69,14 @@ class ConfigureEventDetails(
}
private fun isActionButtonVisible(isEventCompleted: Boolean, storedEvent: Event?): Boolean {
- return storedEvent?.let {
- !(it.status() == OVERDUE && enrollmentStatus == CANCELLED) &&
- repository.getEditableStatus() !is NonEditable
- } ?: isEventCompleted
+ return if (!isEventCompleted) {
+ false
+ } else {
+ storedEvent?.let {
+ !(it.status() == OVERDUE && enrollmentStatus == CANCELLED) &&
+ repository.getEditableStatus() !is NonEditable
+ } ?: true
+ }
}
fun reopenEvent() = repository.reopenEvent()
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt
new file mode 100644
index 0000000000..cc4f28bd4e
--- /dev/null
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt
@@ -0,0 +1,382 @@
+package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers
+
+import androidx.compose.foundation.background
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ExposedDropdownMenuBox
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import org.dhis2.R
+import org.dhis2.commons.resources.ResourceManager
+import org.dhis2.data.dhislogic.inDateRange
+import org.dhis2.data.dhislogic.inOrgUnit
+import org.dhis2.form.model.UiEventType
+import org.dhis2.form.model.UiRenderType
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus
+import org.dhis2.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT
+import org.hisp.dhis.android.core.arch.helpers.GeometryHelper
+import org.hisp.dhis.android.core.arch.helpers.Result
+import org.hisp.dhis.android.core.category.CategoryOption
+import org.hisp.dhis.android.core.common.FeatureType
+import org.hisp.dhis.android.core.common.Geometry
+import org.hisp.dhis.android.core.common.ValueType
+import org.hisp.dhis.mobile.ui.designsystem.component.Coordinates
+import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionIconType
+import org.hisp.dhis.mobile.ui.designsystem.component.InputCoordinate
+import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTime
+import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown
+import org.hisp.dhis.mobile.ui.designsystem.component.InputOrgUnit
+import org.hisp.dhis.mobile.ui.designsystem.component.InputPolygon
+import org.hisp.dhis.mobile.ui.designsystem.component.InputRadioButton
+import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState
+import org.hisp.dhis.mobile.ui.designsystem.component.Orientation
+import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData
+import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation
+import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor
+import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
+import java.util.Date
+
+@Composable
+fun ProvideInputDate(
+ eventDate: EventDate,
+ detailsEnabled: Boolean,
+ onDateClick: () -> Unit,
+ allowsManualInput: Boolean = true,
+ onDateSet: (InputDateValues) -> Unit,
+ onClear: () -> Unit,
+ required: Boolean = false,
+) {
+ var value by remember(eventDate.dateValue) {
+ mutableStateOf(eventDate.dateValue?.let { formatStoredDateToUI(it) })
+ }
+
+ var state by remember {
+ mutableStateOf(getInputState(detailsEnabled))
+ }
+
+ InputDateTime(
+ title = eventDate.label ?: "",
+ allowsManualInput = allowsManualInput,
+ value = value,
+ actionIconType = DateTimeActionIconType.DATE,
+ onActionClicked = onDateClick,
+ state = state,
+ visualTransformation = DateTransformation(),
+ onValueChanged = {
+ value = it
+ if (it.isEmpty()) {
+ onClear()
+ } else if (isValid(it)) {
+ if (isValidDateFormat(it)) {
+ state = InputShellState.FOCUSED
+ formatUIDateToStored(it)?.let { dateValues ->
+ onDateSet(dateValues)
+ }
+ } else {
+ state = InputShellState.ERROR
+ }
+ } else {
+ state = InputShellState.FOCUSED
+ }
+ },
+ isRequired = required,
+ )
+}
+
+fun isValidDateFormat(dateString: String): Boolean {
+ val year = dateString.substring(4, 8)
+ val month = dateString.substring(2, 4)
+ val day = dateString.substring(0, 2)
+
+ val formattedDate = "$year-$month-$day"
+
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
+
+ return try {
+ LocalDate.parse(formattedDate, formatter)
+ when (ValueType.DATE.validator.validate(formattedDate)) {
+ is Result.Failure -> false
+ is Result.Success -> true
+ }
+ } catch (e: DateTimeParseException) {
+ false
+ }
+}
+
+private fun isValid(valueString: String) = valueString.length == 8
+
+private fun formatStoredDateToUI(dateValue: String): String? {
+ val components = dateValue.split("/")
+ if (components.size != 3) {
+ return null
+ }
+
+ val year = components[2]
+ val month = if (components[1].length == 1) {
+ "0${components[1]}"
+ } else {
+ components[1]
+ }
+ val day = if (components[0].length == 1) {
+ "0${components[0]}"
+ } else {
+ components[0]
+ }
+
+ return "$day$month$year"
+}
+
+fun formatUIDateToStored(dateValue: String?): InputDateValues? {
+ if (dateValue?.length != 8) {
+ return null
+ }
+
+ val year = dateValue.substring(4, 8).toInt()
+ val month = dateValue.substring(2, 4).toInt()
+ val day = dateValue.substring(0, 2).toInt()
+
+ return (InputDateValues(day, month, year))
+}
+
+data class InputDateValues(val day: Int, val month: Int, val year: Int)
+
+@Composable
+fun ProvideOrgUnit(
+ orgUnit: EventOrgUnit,
+ detailsEnabled: Boolean,
+ onOrgUnitClick: () -> Unit,
+ resources: ResourceManager,
+ onClear: () -> Unit,
+ required: Boolean = false,
+) {
+ val state = getInputState(detailsEnabled && orgUnit.enable && orgUnit.orgUnits.size > 1)
+
+ var inputFieldValue by remember(orgUnit.selectedOrgUnit) {
+ mutableStateOf(orgUnit.selectedOrgUnit?.displayName())
+ }
+
+ InputOrgUnit(
+ title = resources.getString(R.string.org_unit),
+ state = state,
+ inputText = inputFieldValue ?: "",
+ onValueChanged = {
+ inputFieldValue = it
+ if (it.isNullOrEmpty()) {
+ onClear()
+ }
+ },
+ onOrgUnitActionCLicked = onOrgUnitClick,
+ isRequiredField = required,
+ )
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun ProvideCategorySelector(
+ modifier: Modifier = Modifier,
+ category: EventCategory,
+ eventCatCombo: EventCatCombo,
+ detailsEnabled: Boolean,
+ currentDate: Date?,
+ selectedOrgUnit: String?,
+ onShowCategoryDialog: (EventCategory) -> Unit,
+ onClearCatCombo: (EventCategory) -> Unit,
+ onOptionSelected: (CategoryOption?) -> Unit,
+ required: Boolean = false,
+) {
+ var selectedItem by remember {
+ mutableStateOf(
+ eventCatCombo.selectedCategoryOptions[category.uid]?.displayName()
+ ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName(),
+ )
+ }
+
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {},
+ ) {
+ InputDropDown(
+ modifier = modifier,
+ title = category.name,
+ state = getInputState(detailsEnabled),
+ selectedItem = selectedItem,
+ onResetButtonClicked = {
+ selectedItem = null
+ onClearCatCombo(category)
+ },
+ onArrowDropDownButtonClicked = {
+ expanded = !expanded
+ },
+ isRequiredField = required,
+ )
+
+ if (expanded) {
+ if (category.optionsSize > DEFAULT_COUNT_LIMIT) {
+ onShowCategoryDialog(category)
+ expanded = false
+ } else {
+ DropdownMenu(
+ modifier = modifier.exposedDropdownSize(),
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ val selectableOptions = category.options
+ .filter { option ->
+ option.access().data().write()
+ }.filter { option ->
+ option.inDateRange(currentDate)
+ }.filter { option ->
+ option.inOrgUnit(selectedOrgUnit)
+ }
+ selectableOptions.forEach { option ->
+ val isSelected = option.displayName() == selectedItem
+ DropdownMenuItem(
+ modifier = Modifier.background(
+ when {
+ isSelected -> SurfaceColor.PrimaryContainer
+ else -> Color.Transparent
+ },
+ ),
+ content = {
+ Text(
+ text = option.displayName() ?: option.code() ?: "",
+ color = when {
+ isSelected -> TextColor.OnPrimaryContainer
+ else -> TextColor.OnSurface
+ },
+ )
+ },
+ onClick = {
+ expanded = false
+ selectedItem = option.displayName()
+ onOptionSelected(option)
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun getInputState(enabled: Boolean) = if (enabled) {
+ InputShellState.UNFOCUSED
+} else {
+ InputShellState.DISABLED
+}
+
+@Composable
+fun ProvideCoordinates(
+ coordinates: EventCoordinates,
+ detailsEnabled: Boolean,
+ resources: ResourceManager,
+) {
+ when (coordinates.model?.renderingType) {
+ UiRenderType.POLYGON, UiRenderType.MULTI_POLYGON -> {
+ InputPolygon(
+ title = resources.getString(R.string.polygon),
+ state = getInputState(detailsEnabled && coordinates.model.editable),
+ polygonAdded = !coordinates.model.value.isNullOrEmpty(),
+ onResetButtonClicked = { coordinates.model.onClear() },
+ onUpdateButtonClicked = {
+ coordinates.model.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP)
+ },
+ )
+ }
+
+ else -> {
+ InputCoordinate(
+ title = resources.getString(R.string.coordinates),
+ state = getInputState(detailsEnabled && coordinates.model?.editable == true),
+ coordinates = mapGeometry(coordinates.model?.value, FeatureType.POINT),
+ latitudeText = resources.getString(R.string.latitude),
+ longitudeText = resources.getString(R.string.longitude),
+ addLocationBtnText = resources.getString(R.string.add_location),
+ onResetButtonClicked = {
+ coordinates.model?.onClear()
+ },
+ onUpdateButtonClicked = {
+ coordinates.model?.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP)
+ },
+ )
+ }
+ }
+}
+
+fun mapGeometry(value: String?, featureType: FeatureType): Coordinates? {
+ return value?.let {
+ val geometry = Geometry.builder()
+ .coordinates(it)
+ .type(featureType)
+ .build()
+
+ Coordinates(
+ latitude = GeometryHelper.getPoint(geometry)[1],
+ longitude = GeometryHelper.getPoint(geometry)[0],
+ )
+ }
+}
+
+@Composable
+fun ProvideRadioButtons(
+ eventTemp: EventTemp,
+ detailsEnabled: Boolean,
+ resources: ResourceManager,
+ onEventTempSelected: (status: EventTempStatus?) -> Unit,
+) {
+ val radioButtonData = listOf(
+ RadioButtonData(
+ uid = EventTempStatus.ONE_TIME.name,
+ selected = eventTemp.status == EventTempStatus.ONE_TIME,
+ enabled = true,
+ textInput = resources.getString(R.string.one_time),
+ ),
+ RadioButtonData(
+ uid = EventTempStatus.PERMANENT.name,
+ selected = eventTemp.status == EventTempStatus.PERMANENT,
+ enabled = true,
+ textInput = resources.getString(R.string.permanent),
+ ),
+ )
+
+ InputRadioButton(
+ title = resources.getString(R.string.referral),
+ radioButtonData = radioButtonData,
+ orientation = Orientation.HORIZONTAL,
+ state = getInputState(detailsEnabled),
+ itemSelected = radioButtonData.find { it.selected },
+ onItemChange = { data ->
+ when (data?.uid) {
+ EventTempStatus.ONE_TIME.name -> {
+ onEventTempSelected(EventTempStatus.ONE_TIME)
+ }
+
+ EventTempStatus.PERMANENT.name -> {
+ onEventTempSelected(EventTempStatus.PERMANENT)
+ }
+
+ else -> {
+ onEventTempSelected(null)
+ }
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt
index f49ccc1438..171e6fa43c 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt
@@ -8,6 +8,13 @@ import android.view.View
import android.view.ViewGroup
import android.widget.DatePicker
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
@@ -27,16 +34,21 @@ import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener
import org.dhis2.commons.locationprovider.LocationSettingLauncher
import org.dhis2.commons.orgunitselector.OUTreeFragment
import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope
+import org.dhis2.commons.resources.ResourceManager
import org.dhis2.databinding.EventDetailsFragmentBinding
import org.dhis2.maps.views.MapSelectorActivity
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCategorySelector
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCoordinates
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideOrgUnit
+import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideRadioButtons
import org.dhis2.usescases.general.FragmentGlobalAbstract
import org.dhis2.utils.category.CategoryDialog
import org.dhis2.utils.category.CategoryDialog.Companion.TAG
-import org.dhis2.utils.customviews.CatOptionPopUp
import org.dhis2.utils.customviews.PeriodDialog
import org.hisp.dhis.android.core.common.FeatureType
import org.hisp.dhis.android.core.enrollment.EnrollmentStatus
@@ -49,6 +61,9 @@ class EventDetailsFragment : FragmentGlobalAbstract() {
@Inject
lateinit var factory: EventDetailsViewModelFactory
+ @Inject
+ lateinit var resourceManager: ResourceManager
+
private val requestLocationPermissions =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
@@ -119,6 +134,89 @@ class EventDetailsFragment : FragmentGlobalAbstract() {
)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
+ binding.fieldsContainer.setContent {
+ val date by viewModel.eventDate.collectAsState()
+ val details by viewModel.eventDetails.collectAsState()
+ val orgUnit by viewModel.eventOrgUnit.collectAsState()
+ val catCombo by viewModel.eventCatCombo.collectAsState()
+ val coordinates by viewModel.eventCoordinates.collectAsState()
+ val eventTemp by viewModel.eventTemp.collectAsState()
+
+ Column {
+ if (date.active) {
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideInputDate(
+ eventDate = date,
+ detailsEnabled = details.enabled,
+ onDateClick = { viewModel.onDateClick() },
+ onDateSet = { dateValues ->
+ viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day)
+ },
+ onClear = { viewModel.onClearEventReportDate() },
+ required = true,
+ )
+ }
+ if (orgUnit.visible) {
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideOrgUnit(
+ orgUnit = orgUnit,
+ detailsEnabled = details.enabled,
+ onOrgUnitClick = { viewModel.onOrgUnitClick() },
+ resources = resourceManager,
+ onClear = {
+ viewModel.onClearOrgUnit()
+ },
+ required = true,
+ )
+ }
+
+ if (!catCombo.isDefault) {
+ catCombo.categories.forEach { category ->
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideCategorySelector(
+ category = category,
+ eventCatCombo = catCombo,
+ detailsEnabled = details.enabled,
+ currentDate = date.currentDate,
+ selectedOrgUnit = details.selectedOrgUnit,
+ onShowCategoryDialog = {
+ showCategoryDialog(it)
+ },
+ onClearCatCombo = {
+ viewModel.onClearCatCombo()
+ },
+ onOptionSelected = {
+ val selectedOption = Pair(category.uid, it?.uid())
+ viewModel.setUpCategoryCombo(selectedOption)
+ },
+
+ required = true,
+ )
+ }
+ }
+
+ if (coordinates.active) {
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideCoordinates(
+ coordinates = coordinates,
+ detailsEnabled = details.enabled,
+ resources = resourceManager,
+ )
+ }
+
+ if (eventTemp.active) {
+ Spacer(modifier = Modifier.height(16.dp))
+ ProvideRadioButtons(
+ eventTemp = eventTemp,
+ detailsEnabled = details.enabled,
+ resources = resourceManager,
+ onEventTempSelected = {
+ viewModel.setUpEventTemp(it)
+ },
+ )
+ }
+ }
+ }
return binding.root
}
@@ -147,14 +245,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() {
showNoOrgUnitsDialog()
}
- viewModel.showCategoryDialog = { category ->
- showCategoryDialog(category)
- }
-
- viewModel.showCategoryPopUp = { category ->
- showCategoryPopUp(category)
- }
-
viewModel.requestLocationPermissions = {
requestLocationPermissions.launch(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
@@ -247,7 +337,7 @@ class EventDetailsFragment : FragmentGlobalAbstract() {
OrgUnitSelectorScope.ProgramCaptureScope(viewModel.eventOrgUnit.value.programUid!!),
)
.onSelection { selectedOrgUnits ->
- viewModel.setUpOrgUnit(selectedOrgUnit = selectedOrgUnits.first().uid())
+ viewModel.setUpOrgUnit(selectedOrgUnit = selectedOrgUnits.firstOrNull()?.uid())
}
.build()
.show(childFragmentManager, "ORG_UNIT_DIALOG")
@@ -257,19 +347,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() {
showInfoDialog(getString(R.string.error), getString(R.string.no_org_units))
}
- private fun showCategoryPopUp(category: EventCategory) {
- CatOptionPopUp(
- context = requireContext(),
- anchor = binding.catComboLayout,
- options = category.options,
- date = viewModel.eventDate.value.currentDate,
- orgUnitUid = viewModel.eventDetails.value.selectedOrgUnit,
- ) { categoryOption ->
- val selectedOption = Pair(category.uid, categoryOption?.uid())
- viewModel.setUpCategoryCombo(selectedOption)
- }.show()
- }
-
private fun showCategoryDialog(category: EventCategory) {
CategoryDialog(
CategoryDialog.Type.CATEGORY_OPTIONS,
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt
index f06c04c41b..a740d4c70d 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt
@@ -1,7 +1,5 @@
package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui
-import android.view.LayoutInflater
-import android.widget.LinearLayout
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -9,36 +7,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.databinding.BindingAdapter
import com.google.android.material.composethemeadapter.MdcTheme
-import org.dhis2.databinding.CategorySelectorBinding
-import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo
-
-@BindingAdapter(value = ["setViewModel", "setCatCombo", "enabled"])
-fun LinearLayout.setCatCombo(
- viewModel: EventDetailsViewModel,
- eventCatCombo: EventCatCombo,
- enabled: Boolean?,
-) {
- if (!eventCatCombo.isDefault) {
- this@setCatCombo.removeAllViews()
- eventCatCombo.categories.forEach { category ->
- val catSelectorBinding: CategorySelectorBinding =
- CategorySelectorBinding.inflate(LayoutInflater.from(context))
- catSelectorBinding.catCombLayout.hint = category.name
- catSelectorBinding.catCombo.isEnabled = enabled ?: true
- catSelectorBinding.catCombo.setOnClickListener {
- viewModel.onCatComboClick(category)
- }
-
- val selectorDisplay =
- eventCatCombo.selectedCategoryOptions[category.uid]?.displayName()
- ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName()
-
- catSelectorBinding.catCombo.setText(selectorDisplay)
-
- this@setCatCombo.addView(catSelectorBinding.root)
- }
- }
-}
@ExperimentalAnimationApi
@BindingAdapter("setReopen")
diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt
index 290ad4e1dc..614e1232a8 100644
--- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt
+++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt
@@ -18,7 +18,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo
-import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails
@@ -26,7 +25,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOr
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider
-import org.dhis2.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT
import org.hisp.dhis.android.core.arch.helpers.GeometryHelper
import org.hisp.dhis.android.core.common.FeatureType
import org.hisp.dhis.android.core.common.Geometry
@@ -53,8 +51,6 @@ class EventDetailsViewModel(
var showPeriods: (() -> Unit)? = null
var showOrgUnits: (() -> Unit)? = null
var showNoOrgUnits: (() -> Unit)? = null
- var showCategoryDialog: ((category: EventCategory) -> Unit)? = null
- var showCategoryPopUp: ((category: EventCategory) -> Unit)? = null
var requestLocationPermissions: (() -> Unit)? = null
var showEnableLocationMessage: (() -> Unit)? = null
var requestLocationByMap: ((featureType: String, initCoordinate: String?) -> Unit)? = null
@@ -171,6 +167,11 @@ class EventDetailsViewModel(
}
}
+ fun onClearEventReportDate() {
+ _eventDate.value = eventDate.value.copy(currentDate = null)
+ setUpEventDetails()
+ }
+
fun setUpOrgUnit(selectedDate: Date? = null, selectedOrgUnit: String? = null) {
viewModelScope.launch {
configureOrgUnit(selectedDate, selectedOrgUnit)
@@ -182,6 +183,11 @@ class EventDetailsViewModel(
}
}
+ fun onClearOrgUnit() {
+ _eventOrgUnit.value = eventOrgUnit.value.copy(selectedOrgUnit = null)
+ setUpEventDetails()
+ }
+
fun setUpCategoryCombo(categoryOption: Pair? = null) {
EventDetailIdlingResourceSingleton.increment()
viewModelScope.launch {
@@ -195,6 +201,11 @@ class EventDetailsViewModel(
}
}
+ fun onClearCatCombo() {
+ _eventCatCombo.value = eventCatCombo.value.copy(isCompleted = false)
+ setUpEventDetails()
+ }
+
private fun setUpCoordinates(value: String? = "") {
EventDetailIdlingResourceSingleton.increment()
viewModelScope.launch {
@@ -256,14 +267,6 @@ class EventDetailsViewModel(
}
}
- fun onCatComboClick(category: EventCategory) {
- if (category.optionsSize > DEFAULT_COUNT_LIMIT) {
- showCategoryDialog?.invoke(category)
- } else {
- showCategoryPopUp?.invoke(category)
- }
- }
-
fun requestCurrentLocation() {
locationProvider.getLastKnownLocation(
onNewLocation = { location ->
@@ -365,5 +368,6 @@ inline fun Result.mockSafeFold(
}
}
}
+
else -> onFailure(exceptionOrNull() ?: Exception())
}
diff --git a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt
index 4304569c38..a4f8ab5539 100644
--- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt
+++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt
@@ -49,6 +49,7 @@ import java.io.File
import javax.inject.Inject
private const val FRAGMENT = "Fragment"
+private const val SINGLE_PROGRAM_NAVIGATION = "SINGLE_PROGRAM_NAVIGATION"
private const val INIT_DATA_SYNC = "INIT_DATA_SYNC"
private const val WIPE_NOTIFICATION = "wipe_notification"
private const val RESTART = "Restart"
@@ -73,6 +74,7 @@ class MainActivity :
var notification: Boolean = false
var forceToNotSynced = false
+ private var singleProgramNavigationDone = false
private val getDevActivityContent =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -184,6 +186,7 @@ class MainActivity :
elevation = ViewCompat.getElevation(binding.toolbar)
val restoreScreenName = savedInstanceState?.getString(FRAGMENT)
+ singleProgramNavigationDone = savedInstanceState?.getBoolean(SINGLE_PROGRAM_NAVIGATION) ?: false
val openScreen = intent.getStringExtra(FRAGMENT)
when {
@@ -212,6 +215,8 @@ class MainActivity :
if (!presenter.wasSyncAlreadyDone()) {
presenter.launchInitialDataSync()
+ } else if (!singleProgramNavigationDone && presenter.hasOneHomeItem()) {
+ navigateToSingleProgram()
}
checkNotificationPermission()
@@ -225,6 +230,7 @@ class MainActivity :
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
+ outState.putBoolean(SINGLE_PROGRAM_NAVIGATION, singleProgramNavigationDone)
outState.putString(FRAGMENT, mainNavigator.currentScreenName())
}
@@ -245,15 +251,21 @@ class MainActivity :
private fun observeSyncState() {
presenter.observeDataSync().observe(this) {
- if (it.running) {
- setFilterButtonVisibility(false)
- setBottomNavigationVisibility(false)
- } else {
- setFilterButtonVisibility(true)
- setBottomNavigationVisibility(true)
- presenter.onDataSuccess()
- if (presenter.hasOneHomeItem()) {
- navigateToSingleProgram()
+ when (it.running) {
+ true -> {
+ setFilterButtonVisibility(false)
+ setBottomNavigationVisibility(false)
+ }
+ false -> {
+ setFilterButtonVisibility(true)
+ setBottomNavigationVisibility(true)
+ presenter.onDataSuccess()
+ if (presenter.hasOneHomeItem()) {
+ navigateToSingleProgram()
+ }
+ }
+ else -> {
+ // no action
}
}
}
@@ -261,6 +273,7 @@ class MainActivity :
private fun navigateToSingleProgram() {
presenter.getSingleItemData()?.let { homeItemData ->
+ singleProgramNavigationDone = true
navigationLauncher.navigateTo(this, homeItemData)
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt
index 8bb4959877..80861f6011 100644
--- a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt
+++ b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt
@@ -197,6 +197,7 @@ class MainPresenter(
disposable.add(
Completable.fromCallable {
workManagerController.cancelAllWork()
+ syncStatusController.restore()
FilterManager.getInstance().clearAllFilters()
preferences.setValue(Preference.SESSION_LOCKED, false)
userManager.d2.dataStoreModule().localDataStore().value(PIN).blockingDeleteIfExist()
@@ -218,6 +219,7 @@ class MainPresenter(
view.showProgressDeleteNotification()
try {
workManagerController.cancelAllWork()
+ syncStatusController.restore()
deleteUserData.wipeCacheAndPreferences(view.obtainFileView())
userManager.d2?.wipeModule()?.wipeEverything()
userManager.d2?.userModule()?.accountManager()?.deleteCurrentAccount()
diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt
index 9498a55577..8bc25cf6f1 100644
--- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt
+++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt
@@ -166,7 +166,7 @@ internal class ProgramRepositoryImpl(
else ->
ProgramDownloadState.NONE
},
- downloadActive = syncStatusData.running,
+ downloadActive = syncStatusData.running ?: false,
)
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfigurator.kt
index 9e3ae7172a..7454e811b9 100644
--- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfigurator.kt
+++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfigurator.kt
@@ -1,6 +1,7 @@
package org.dhis2.usescases.searchTrackEntity
import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator
+import org.hisp.dhis.android.core.common.FeatureType
class SearchPageConfigurator(
val searchRepository: SearchRepository,
@@ -10,8 +11,8 @@ class SearchPageConfigurator(
private var canDisplayAnalytics: Boolean = false
fun initVariables(): SearchPageConfigurator {
- canDisplayMap = searchRepository.programHasCoordinates()
- canDisplayAnalytics = searchRepository.programHasAnalytics()
+ canDisplayMap = programHasCoordinates()
+ canDisplayAnalytics = programHasAnalytics()
return this
}
@@ -30,4 +31,47 @@ class SearchPageConfigurator(
override fun displayAnalytics(): Boolean {
return canDisplayAnalytics
}
+
+ internal fun programHasCoordinates(): Boolean {
+ val program = searchRepository.currentProgram()?.let { programId ->
+ searchRepository.getProgram(programId)
+ } ?: return false
+
+ val programHasCoordinates = program.featureType() != null && program.featureType() != FeatureType.NONE
+
+ val programStagesHaveCoordinates by lazy {
+ searchRepository.programStagesHaveCoordinates(program.uid())
+ }
+
+ val programAttributesHaveCoordinates by lazy {
+ searchRepository.programAttributesHaveCoordinates(program.uid())
+ }
+
+ val teTypeHasCoordinates by lazy {
+ searchRepository.trackedEntityType?.let { teType ->
+ teType.featureType() != null && teType.featureType() != FeatureType.NONE
+ } ?: false
+ }
+
+ val teAttributesHaveCoordinates by lazy {
+ searchRepository.teTypeAttributesHaveCoordinates(program.trackedEntityType()?.uid())
+ }
+
+ val eventsHaveCoordinates by lazy {
+ searchRepository.eventsHaveCoordinates(program.uid())
+ }
+
+ return programHasCoordinates ||
+ programStagesHaveCoordinates ||
+ programAttributesHaveCoordinates ||
+ teTypeHasCoordinates ||
+ teAttributesHaveCoordinates ||
+ eventsHaveCoordinates
+ }
+
+ internal fun programHasAnalytics(): Boolean {
+ val programUid = searchRepository.currentProgram() ?: return false
+
+ return searchRepository.getProgramVisualizationGroups(programUid).isNotEmpty()
+ }
}
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 11df1c23b5..a1dc2103a4 100644
--- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java
+++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java
@@ -13,6 +13,7 @@
import org.hisp.dhis.android.core.arch.call.D2Progress;
import org.hisp.dhis.android.core.organisationunit.OrganisationUnit;
import org.hisp.dhis.android.core.program.Program;
+import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationsGroup;
import org.hisp.dhis.android.core.trackedentity.TrackedEntityType;
import org.jetbrains.annotations.NotNull;
@@ -58,10 +59,15 @@ public interface SearchRepository {
TeiDownloadResult download(String teiUid, @Nullable String enrollmentUid, String reason);
void setCurrentProgram(@Nullable String currentProgram);
- boolean programHasAnalytics();
- boolean programHasCoordinates();
+ boolean programStagesHaveCoordinates(String programUid);
+ boolean teTypeAttributesHaveCoordinates(String typeId);
+ boolean programAttributesHaveCoordinates(String programUid);
+ boolean eventsHaveCoordinates(String programUid);
+
+ List getProgramVisualizationGroups(String programUid);
@Nullable Program getProgram(@Nullable String programUid);
+ @Nullable String currentProgram();
@NotNull Map filterQueryForProgram(@NotNull Map queryData, @org.jetbrains.annotations.Nullable String programUid);
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 4ebecef374..967ffa2c3a 100644
--- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java
+++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java
@@ -9,9 +9,9 @@
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
-import org.dhis2.bindings.ValueExtensionsKt;
import org.dhis2.R;
import org.dhis2.bindings.ExtensionsKt;
+import org.dhis2.bindings.ValueExtensionsKt;
import org.dhis2.commons.Constants;
import org.dhis2.commons.data.EntryMode;
import org.dhis2.commons.data.EventViewModel;
@@ -69,6 +69,7 @@
import org.hisp.dhis.android.core.relationship.RelationshipItem;
import org.hisp.dhis.android.core.relationship.RelationshipItemTrackedEntityInstance;
import org.hisp.dhis.android.core.relationship.RelationshipType;
+import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationsGroup;
import org.hisp.dhis.android.core.settings.ProgramConfigurationSetting;
import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute;
import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue;
@@ -765,7 +766,7 @@ private SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable P
}
}
- searchTei.setOnline(false);
+ searchTei.setOnline(!searchItem.isOnline());
if (offlineOnly)
searchTei.setOnline(!offlineOnly);
@@ -836,103 +837,67 @@ public void setCurrentProgram(String currentProgram) {
this.currentProgram = currentProgram;
}
- private String currentProgram() {
+ @Override
+ public String currentProgram() {
return currentProgram;
}
@Override
- public boolean programHasAnalytics() {
- String programUid = currentProgram();
- if (programUid != null) {
- boolean hasCharts = charts != null && !charts.getProgramVisualizations(null, programUid).isEmpty();
- return hasCharts;
+ public List getProgramVisualizationGroups(String programUid) {
+ if (charts != null) {
+ return charts.getVisualizationGroups(programUid);
} else {
- return false;
+ return Collections.emptyList();
}
}
@Override
- public boolean programHasCoordinates() {
- String programUid = currentProgram();
-
- if (programUid == null) return false;
-
- boolean teTypeHasCoordinates = false;
- FeatureType teTypeFeatureType = d2.trackedEntityModule().trackedEntityTypes()
- .uid(teiType)
- .blockingGet()
- .featureType();
-
- if (teTypeFeatureType != null && teTypeFeatureType != FeatureType.NONE) {
- teTypeHasCoordinates = true;
- }
-
- boolean enrollmentHasCoordinates = false;
- FeatureType enrollmentFeatureType = d2.programModule().programs()
- .uid(programUid)
- .blockingGet()
- .featureType();
-
- if (enrollmentFeatureType != null && enrollmentFeatureType != FeatureType.NONE) {
- enrollmentHasCoordinates = true;
- }
+ public boolean programStagesHaveCoordinates(String programUid) {
+ return !d2.programModule().programStages()
+ .byProgramUid().eq(programUid)
+ .byFeatureType().notIn(FeatureType.NONE)
+ .blockingIsEmpty();
+ }
+ @Override
+ public boolean teTypeAttributesHaveCoordinates(String typeId) {
List teAttributes = d2.trackedEntityModule().trackedEntityTypeAttributes()
- .byTrackedEntityTypeUid().eq(teiType)
+ .byTrackedEntityTypeUid().eq(typeId)
.blockingGet();
List teAttributeUids = new ArrayList<>();
for (TrackedEntityTypeAttribute teTypeAttr : teAttributes) {
teAttributeUids.add(teTypeAttr.trackedEntityAttribute().uid());
}
- boolean teAttributeHasCoordinates = !d2.trackedEntityModule().trackedEntityAttributes()
+ return !d2.trackedEntityModule().trackedEntityAttributes()
.byUid().in(teAttributeUids)
- .byValueType().eq(ValueType.COORDINATE)
+ .byValueType().in(ValueType.COORDINATE, ValueType.GEOJSON)
.blockingIsEmpty();
+ }
- boolean programAttributeHasCoordinates = false;
- boolean eventHasCoordinates = false;
- boolean eventDataElementHasCoordinates = false;
- if (programUid != null) {
- List programAttributes = d2.programModule().programTrackedEntityAttributes()
- .byProgram().eq(programUid)
- .blockingGet();
- List programAttributeUids = new ArrayList<>();
- for (ProgramTrackedEntityAttribute programAttr : programAttributes) {
- programAttributeUids.add(programAttr.trackedEntityAttribute().uid());
- }
-
- programAttributeHasCoordinates = !d2.trackedEntityModule().trackedEntityAttributes()
- .byUid().in(programAttributeUids)
- .byValueType().eq(ValueType.COORDINATE)
- .blockingIsEmpty();
-
- eventHasCoordinates = !d2.programModule().programStages()
- .byProgramUid().eq(programUid)
- .byFeatureType().notIn(FeatureType.NONE)
- .blockingIsEmpty();
-
-
- List events = d2.eventModule().eventQuery().byIncludeDeleted()
- .eq(false)
- .byProgram()
- .eq(programUid)
- .blockingGet();
- for (Event event : events) {
- if (event.geometry() != null) {
- eventDataElementHasCoordinates = true;
- break;
- }
- }
-
+ @Override
+ public boolean programAttributesHaveCoordinates(String programUid) {
+ List programAttributes = d2.programModule().programTrackedEntityAttributes()
+ .byProgram().eq(programUid)
+ .blockingGet();
+ List programAttributeUids = new ArrayList<>();
+ for (ProgramTrackedEntityAttribute programAttr : programAttributes) {
+ programAttributeUids.add(programAttr.trackedEntityAttribute().uid());
}
- return teTypeHasCoordinates ||
- enrollmentHasCoordinates ||
- teAttributeHasCoordinates ||
- programAttributeHasCoordinates ||
- eventHasCoordinates ||
- eventDataElementHasCoordinates;
+ return !d2.trackedEntityModule().trackedEntityAttributes()
+ .byUid().in(programAttributeUids)
+ .byValueType().in(ValueType.COORDINATE, ValueType.GEOJSON)
+ .blockingIsEmpty();
+ }
+
+ @Override
+ public boolean eventsHaveCoordinates(String programUid) {
+ return !d2.eventModule().events()
+ .byDeleted().isFalse()
+ .byProgramUid().eq(programUid)
+ .byGeometryCoordinates().isNotNull()
+ .blockingIsEmpty();
}
@Nullable
diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt
index e1b6e5375c..29e5b86bb3 100644
--- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt
+++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt
@@ -85,12 +85,12 @@ class SearchTeiLiveAdapter(
if (it.isOnline) {
onDownloadTei.invoke(
it.tei.uid(),
- it.selectedEnrollment.uid(),
+ it.selectedEnrollment?.uid(),
)
} else {
onTeiClick.invoke(
it.tei.uid(),
- it.selectedEnrollment.uid(),
+ it.selectedEnrollment?.uid(),
it.isOnline,
)
}
diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt
index 9ba650f305..ba4837b7e9 100644
--- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt
+++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt
@@ -128,7 +128,7 @@ class TEICardMapper(
)
checkEnrollmentStatus(
list = list,
- status = searchTEIModel.selectedEnrollment.status(),
+ status = searchTEIModel.selectedEnrollment?.status(),
)
checkOverdue(
@@ -218,7 +218,7 @@ class TEICardMapper(
) {
val programNames = enrolledPrograms.map { it.name() }
- if (programNames.size > 1) {
+ if (programNames.isNotEmpty()) {
list.add(
AdditionalInfoItem(
key = resourceManager.getString(R.string.programs),
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 e1acc90837..fda6a377e7 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt
@@ -182,7 +182,6 @@ class TeiDashboardMobileActivity :
getString(R.string.msg_network_connection_maps),
)
}
- binding.syncButton.visibility = if (programUid != null) View.VISIBLE else View.GONE
binding.syncButton.setOnClickListener { openSyncDialog() }
if (intent.shouldLaunchSyncDialog()) {
openSyncDialog()
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt
index 83eb34b1d5..78d3e1941b 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt
@@ -268,6 +268,12 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View {
)
val card = teiDashboardCardMapper.map(
dashboardModel = dashboardModel,
+ onImageClick = { fileToShow ->
+ ImageDetailBottomDialog(
+ null,
+ fileToShow,
+ ).show(childFragmentManager, ImageDetailBottomDialog.TAG)
+ },
phoneCallback = { openChooser(it, Intent.ACTION_DIAL) },
emailCallback = { openChooser(it, Intent.ACTION_SENDTO) },
programsCallback = {
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt
index 3b58f97416..2feff90014 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt
@@ -17,6 +17,7 @@ import org.dhis2.commons.bindings.event
import org.dhis2.commons.bindings.program
import org.dhis2.commons.data.EventCreationType
import org.dhis2.commons.data.EventViewModel
+import org.dhis2.commons.data.EventViewModelType
import org.dhis2.commons.data.StageSection
import org.dhis2.commons.filters.FilterManager
import org.dhis2.commons.filters.data.FilterRepository
@@ -144,13 +145,6 @@ class TEIDataPresenter(
teiDataRepository.getTEIEnrollmentEvents(
stageAndGrouping.first,
stageAndGrouping.second,
- filterManager.periodFilters,
- filterManager.orgUnitUidsFilters,
- filterManager.stateFilters,
- filterManager.assignedFilter,
- filterManager.eventStatusFilters,
- filterManager.catOptComboFilters,
- filterManager.sortingItem,
).toFlowable(),
ruleEngineRepository.updateRuleEngine()
.flatMap { ruleEngineRepository.reCalculate() },
@@ -266,7 +260,12 @@ class TEIDataPresenter(
valueStore,
)
stagesToHide = stagesToHide1
- return events
+ return events.filter {
+ when (it.type) {
+ EventViewModelType.STAGE -> !stagesToHide.contains(it.stage?.uid())
+ EventViewModelType.EVENT -> !stagesToHide.contains(it.event?.programStage())
+ }
+ }
}
@VisibleForTesting
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepository.kt
index 649a208e49..59947a21d9 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepository.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepository.kt
@@ -3,13 +3,8 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata
import io.reactivex.Single
import org.dhis2.commons.data.EventViewModel
import org.dhis2.commons.data.StageSection
-import org.dhis2.commons.filters.sorting.SortingItem
-import org.hisp.dhis.android.core.category.CategoryOptionCombo
-import org.hisp.dhis.android.core.common.State
import org.hisp.dhis.android.core.enrollment.Enrollment
-import org.hisp.dhis.android.core.event.EventStatus
import org.hisp.dhis.android.core.organisationunit.OrganisationUnit
-import org.hisp.dhis.android.core.period.DatePeriod
import org.hisp.dhis.android.core.program.Program
import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance
@@ -17,13 +12,6 @@ interface TeiDataRepository {
fun getTEIEnrollmentEvents(
selectedStage: StageSection,
groupedByStage: Boolean,
- periodFilters: MutableList,
- orgUnitFilters: MutableList,
- stateFilters: MutableList,
- assignedToMe: Boolean,
- eventStatusFilters: MutableList,
- catOptComboFilters: MutableList,
- sortingItem: SortingItem?,
): Single>
fun getEnrollment(): Single
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt
index 9f7c259366..cd3e730370 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt
@@ -1,29 +1,22 @@
package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata
import io.reactivex.Single
-import org.dhis2.bindings.applyFilters
import org.dhis2.bindings.profilePicturePath
import org.dhis2.bindings.userFriendlyValue
import org.dhis2.commons.data.EventViewModel
import org.dhis2.commons.data.EventViewModelType
import org.dhis2.commons.data.StageSection
-import org.dhis2.commons.filters.Filters
-import org.dhis2.commons.filters.sorting.SortingItem
-import org.dhis2.commons.filters.sorting.SortingStatus
import org.dhis2.data.dhislogic.DhisPeriodUtils
import org.dhis2.utils.DateUtils
import org.hisp.dhis.android.core.D2
import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope
import org.hisp.dhis.android.core.category.CategoryCombo
-import org.hisp.dhis.android.core.category.CategoryOptionCombo
-import org.hisp.dhis.android.core.common.State
import org.hisp.dhis.android.core.common.ValueType
import org.hisp.dhis.android.core.enrollment.Enrollment
import org.hisp.dhis.android.core.event.Event
import org.hisp.dhis.android.core.event.EventCollectionRepository
import org.hisp.dhis.android.core.event.EventStatus
import org.hisp.dhis.android.core.organisationunit.OrganisationUnit
-import org.hisp.dhis.android.core.period.DatePeriod
import org.hisp.dhis.android.core.period.PeriodType
import org.hisp.dhis.android.core.program.Program
import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance
@@ -40,33 +33,13 @@ class TeiDataRepositoryImpl(
override fun getTEIEnrollmentEvents(
selectedStage: StageSection,
groupedByStage: Boolean,
- periodFilters: MutableList,
- orgUnitFilters: MutableList,
- stateFilters: MutableList,
- assignedToMe: Boolean,
- eventStatusFilters: MutableList,
- catOptComboFilters: MutableList,
- sortingItem: SortingItem?,
): Single> {
- var eventRepo = d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid)
-
- eventRepo = eventRepo.applyFilters(
- periodFilters,
- orgUnitFilters,
- stateFilters,
- if (assignedToMe) {
- d2.userModule().user().blockingGet()?.uid()
- } else {
- null
- },
- eventStatusFilters,
- catOptComboFilters,
- )
+ val eventRepo = d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid)
return if (groupedByStage) {
- getGroupedEvents(eventRepo, selectedStage, sortingItem)
+ getGroupedEvents(eventRepo, selectedStage)
} else {
- getTimelineEvents(eventRepo, sortingItem)
+ getTimelineEvents(eventRepo)
}
}
@@ -161,13 +134,14 @@ class TeiDataRepositoryImpl(
}
override fun getTeiHeader(): String? {
- return d2.trackedEntityModule().trackedEntitySearch().uid(teiUid).blockingGet()?.header
+ return d2.trackedEntityModule().trackedEntitySearch()
+ .byProgram().eq(programUid)
+ .uid(teiUid).blockingGet()?.header
}
private fun getGroupedEvents(
eventRepository: EventCollectionRepository,
selectedStage: StageSection,
- sortingItem: SortingItem?,
): Single> {
val eventViewModels = mutableListOf()
var eventRepo: EventCollectionRepository
@@ -181,8 +155,9 @@ class TeiDataRepositoryImpl(
eventRepo = eventRepository.byDeleted().isFalse
.byProgramStageUid().eq(programStage.uid())
- eventRepo = eventRepoSorting(sortingItem, eventRepo)
- val eventList = eventRepo.blockingGet()
+ val eventList = eventRepo
+ .orderByTimeline(RepositoryScope.OrderByDirection.DESC)
+ .blockingGet()
val isSelected = programStage.uid() == selectedStage.stageUid
@@ -253,14 +228,11 @@ class TeiDataRepositoryImpl(
private fun getTimelineEvents(
eventRepository: EventCollectionRepository,
- sortingItem: SortingItem?,
): Single> {
val eventViewModels = mutableListOf()
- var eventRepo = eventRepository
-
- eventRepo = eventRepoSorting(sortingItem, eventRepo)
- return eventRepo
+ return eventRepository
+ .orderByTimeline(RepositoryScope.OrderByDirection.DESC)
.byDeleted().isFalse
.get()
.map { eventList ->
@@ -313,39 +285,6 @@ class TeiDataRepositoryImpl(
}
}
- private fun eventRepoSorting(
- sortingItem: SortingItem?,
- eventRepo: EventCollectionRepository,
- ): EventCollectionRepository {
- return if (sortingItem != null) {
- when (sortingItem.filterSelectedForSorting) {
- Filters.ORG_UNIT ->
- if (sortingItem.sortingStatus == SortingStatus.ASC) {
- eventRepo.orderByOrganisationUnitName(RepositoryScope.OrderByDirection.ASC)
- } else {
- eventRepo.orderByOrganisationUnitName(RepositoryScope.OrderByDirection.DESC)
- }
-
- Filters.PERIOD -> {
- if (sortingItem.sortingStatus === SortingStatus.ASC) {
- eventRepo
- .orderByTimeline(RepositoryScope.OrderByDirection.ASC)
- } else {
- eventRepo
- .orderByTimeline(RepositoryScope.OrderByDirection.DESC)
- }
- }
-
- else -> {
- eventRepo
- }
- }
- } else {
- eventRepo
- .orderByTimeline(RepositoryScope.OrderByDirection.DESC)
- }
- }
-
private fun checkEventStatus(events: List): List {
return events.mapNotNull { event ->
if (event.status() == EventStatus.SCHEDULE &&
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDetailDashboard.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDetailDashboard.kt
index 908a9f9d7e..5133a5cd43 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDetailDashboard.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDetailDashboard.kt
@@ -1,5 +1,7 @@
package org.dhis2.usescases.teiDashboard.ui
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -22,53 +24,67 @@ fun TeiDetailDashboard(
) {
LazyColumn(
modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
+ .fillMaxWidth(),
) {
item {
- if (syncData.showInfoBar) {
- InfoBar(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
- .testTag(SYNC_INFO_BAR_TEST_TAG),
- infoBarData =
- InfoBarData(
- text = syncData.text,
- icon = syncData.icon,
- color = syncData.textColor,
- backgroundColor = syncData.backgroundColor,
- actionText = syncData.actionText,
- onClick = syncData.onActionClick,
- ),
- )
- }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ ) {
+ if (syncData.showInfoBar) {
+ InfoBar(
+ modifier = Modifier
+ .padding(start = 8.dp, end = 8.dp)
+ .testTag(SYNC_INFO_BAR_TEST_TAG),
+ infoBarData =
+ InfoBarData(
+ text = syncData.text,
+ icon = syncData.icon,
+ color = syncData.textColor,
+ backgroundColor = syncData.backgroundColor,
+ actionText = syncData.actionText,
+ onClick = syncData.onActionClick,
+ ),
+ )
+ if (followUpData.showInfoBar || enrollmentData.showInfoBar) {
+ Spacer(modifier = Modifier.padding(top = 8.dp))
+ }
+ }
- if (followUpData.showInfoBar) {
- InfoBar(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
- .testTag(FOLLOWUP_INFO_BAR_TEST_TAG),
- infoBarData = InfoBarData(
- text = followUpData.text,
- icon = followUpData.icon,
- color = followUpData.textColor,
- backgroundColor = followUpData.backgroundColor,
- actionText = followUpData.actionText,
- onClick = followUpData.onActionClick,
- ),
- )
- }
+ if (followUpData.showInfoBar) {
+ InfoBar(
+ modifier = Modifier
+ .padding(start = 8.dp, end = 8.dp)
+ .testTag(FOLLOWUP_INFO_BAR_TEST_TAG),
+ infoBarData = InfoBarData(
+ text = followUpData.text,
+ icon = followUpData.icon,
+ color = followUpData.textColor,
+ backgroundColor = followUpData.backgroundColor,
+ actionText = followUpData.actionText,
+ onClick = followUpData.onActionClick,
+ ),
+ )
+ if (enrollmentData.showInfoBar) {
+ Spacer(modifier = Modifier.padding(top = 8.dp))
+ }
+ }
- if (enrollmentData.showInfoBar) {
- InfoBar(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 0.dp)
- .testTag(STATE_INFO_BAR_TEST_TAG),
- infoBarData = InfoBarData(
- text = enrollmentData.text,
- icon = enrollmentData.icon,
- color = enrollmentData.textColor,
- backgroundColor = enrollmentData.backgroundColor,
- actionText = enrollmentData.actionText,
- ),
- )
+ if (enrollmentData.showInfoBar) {
+ InfoBar(
+ modifier = Modifier
+ .padding(start = 8.dp, end = 8.dp)
+ .testTag(STATE_INFO_BAR_TEST_TAG),
+ infoBarData = InfoBarData(
+ text = enrollmentData.text,
+ icon = enrollmentData.icon,
+ color = enrollmentData.textColor,
+ backgroundColor = enrollmentData.backgroundColor,
+ actionText = enrollmentData.actionText,
+ ),
+ )
+ }
}
}
diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt
index c9f042ea4a..cc7f2784f0 100644
--- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt
+++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import org.dhis2.R
+import org.dhis2.commons.data.tuples.Pair
import org.dhis2.commons.date.toUi
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.usescases.teiDashboard.DashboardProgramModel
@@ -16,6 +17,8 @@ import org.dhis2.usescases.teiDashboard.ui.model.TeiCardUiModel
import org.hisp.dhis.android.core.common.ValueType
import org.hisp.dhis.android.core.organisationunit.OrganisationUnit
import org.hisp.dhis.android.core.program.Program
+import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute
+import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue
import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem
import org.hisp.dhis.mobile.ui.designsystem.component.Avatar
import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle
@@ -29,12 +32,13 @@ class TeiDashboardCardMapper(
fun map(
dashboardModel: DashboardProgramModel,
+ onImageClick: (File) -> Unit,
phoneCallback: (String) -> Unit,
emailCallback: (String) -> Unit,
programsCallback: () -> Unit,
): TeiCardUiModel {
val avatar: @Composable (() -> Unit)? = if (dashboardModel.avatarPath.isNotEmpty()) {
- { ProvideAvatar(item = dashboardModel) }
+ { ProvideAvatar(item = dashboardModel, onImageClick) }
} else {
null
}
@@ -42,7 +46,12 @@ class TeiDashboardCardMapper(
return TeiCardUiModel(
avatar = avatar,
title = getTitle(dashboardModel),
- additionalInfo = getAdditionalInfo(dashboardModel, phoneCallback, emailCallback, programsCallback),
+ additionalInfo = getAdditionalInfo(
+ dashboardModel,
+ phoneCallback,
+ emailCallback,
+ programsCallback,
+ ),
actionButton = {},
expandLabelText = resourceManager.getString(R.string.show_more),
shrinkLabelText = resourceManager.getString(R.string.show_less),
@@ -51,13 +60,14 @@ class TeiDashboardCardMapper(
}
@Composable
- private fun ProvideAvatar(item: DashboardProgramModel) {
+ private fun ProvideAvatar(item: DashboardProgramModel, onImageClick: (File) -> Unit) {
val file = File(item.avatarPath)
val bitmap = BitmapFactory.decodeFile(file.absolutePath).asImageBitmap()
val painter = BitmapPainter(bitmap)
Avatar(
imagePainter = painter,
+ onImageClick = { onImageClick.invoke(file) },
style = AvatarStyle.IMAGE,
)
}
@@ -68,8 +78,9 @@ class TeiDashboardCardMapper(
} else if (item.attributes.isEmpty()) {
"-"
} else {
- val key = item.attributes.firstOrNull()?.val0()?.displayFormName()
- val value = item.attributes.firstOrNull()?.val1()?.value()
+ val attribute = item.attributes.filterAttributes().firstOrNull()
+ val key = attribute?.val0()?.displayFormName()
+ val value = attribute?.val1()?.value()
"$key: $value"
}
}
@@ -81,11 +92,7 @@ class TeiDashboardCardMapper(
programsCallback: () -> Unit,
): List {
val attributesList = item.attributes
- .asSequence()
- .filter { it.val0().valueType() != ValueType.IMAGE }
- .filter { it.val0().valueType() != ValueType.COORDINATE }
- .filter { it.val0().valueType() != ValueType.FILE_RESOURCE }
- .filter { it.val1().value()?.isNotEmpty() == true }
+ .filterAttributes()
.map {
if (it.val0().valueType() == ValueType.PHONE_NUMBER) {
AdditionalInfoItem(
@@ -149,7 +156,7 @@ class TeiDashboardCardMapper(
)
}
}.also { list ->
- if (item.enrollmentActivePrograms.size > 1) {
+ if (item.enrollmentActivePrograms.isNotEmpty()) {
addEnrollPrograms(
list,
item.enrollmentActivePrograms,
@@ -216,4 +223,10 @@ class TeiDashboardCardMapper(
),
)
}
+
+ private fun List>.filterAttributes() =
+ this.filter { it.val0().valueType() != ValueType.IMAGE }
+ .filter { it.val0().valueType() != ValueType.COORDINATE }
+ .filter { it.val0().valueType() != ValueType.FILE_RESOURCE }
+ .filter { it.val1().value()?.isNotEmpty() == true }
}
diff --git a/app/src/main/java/org/dhis2/utils/analytics/AnalyticsInterceptor.kt b/app/src/main/java/org/dhis2/utils/analytics/AnalyticsInterceptor.kt
index 109e64ab2f..7f3dde9e55 100644
--- a/app/src/main/java/org/dhis2/utils/analytics/AnalyticsInterceptor.kt
+++ b/app/src/main/java/org/dhis2/utils/analytics/AnalyticsInterceptor.kt
@@ -25,7 +25,11 @@
package org.dhis2.utils.analytics
+import io.reactivex.Single
+import io.reactivex.observers.DisposableSingleObserver
+import io.reactivex.schedulers.Schedulers
import okhttp3.Interceptor
+import okhttp3.Request
import okhttp3.Response
import org.dhis2.BuildConfig
import org.hisp.dhis.android.core.D2Manager
@@ -39,17 +43,32 @@ class AnalyticsInterceptor(private val analyticHelper: AnalyticsHelper) : Interc
val response = chain.proceed(request)
if (response.code >= 400 && isLogged()) {
- analyticHelper.trackMatomoEvent(
- API_CALL,
- "${request.method}_${request.url}",
- "${response.code}_${appVersionName}_${getDhis2Version()}",
- )
+ trackMatomoEvent(request, response)
}
return response
}
- private fun getDhis2Version(): String? {
- return D2Manager.getD2().systemInfoModule().systemInfo().blockingGet()?.version()
+ private fun trackMatomoEvent(request: Request, response: Response) {
+ getDhis2Version()
+ .subscribeOn(Schedulers.io())
+ .subscribe(object : DisposableSingleObserver() {
+ override fun onSuccess(version: String) {
+ analyticHelper.trackMatomoEvent(
+ API_CALL,
+ "${request.method}_${request.url}",
+ "${response.code}_${appVersionName}_$version",
+ )
+ dispose()
+ }
+
+ override fun onError(e: Throwable) {
+ dispose()
+ }
+ })
+ }
+
+ private fun getDhis2Version(): Single {
+ return D2Manager.getD2().systemInfoModule().systemInfo().get().map { it.version() }
}
private fun isLogged(): Boolean {
diff --git a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt
index d6fb3bafd5..2217d01c52 100644
--- a/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt
+++ b/app/src/main/java/org/dhis2/utils/customviews/BreakTheGlassBottomDialog.kt
@@ -14,12 +14,10 @@ import org.dhis2.R
import org.dhis2.commons.resources.ColorType
import org.dhis2.commons.resources.ColorUtils
import org.dhis2.databinding.BreakTheGlassBottomDialogBindingImpl
-import javax.inject.Inject
class BreakTheGlassBottomDialog : BottomSheetDialogFragment() {
- @Inject
- lateinit var colorUtils: ColorUtils
+ val colorUtils: ColorUtils = ColorUtils()
fun setPositiveButton(onClick: ((String) -> Unit)? = null) = apply {
this.positiveOnclick = onClick
diff --git a/app/src/main/res/layout-land/activity_dashboard_mobile.xml b/app/src/main/res/layout-land/activity_dashboard_mobile.xml
index 08dae35d84..71ed810355 100644
--- a/app/src/main/res/layout-land/activity_dashboard_mobile.xml
+++ b/app/src/main/res/layout-land/activity_dashboard_mobile.xml
@@ -73,6 +73,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content" />
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintBottom_toBottomOf="parent" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content" />
El campo es obligatorioPor razones de seguridad, esta versión de la app no permite el uso de adb. Por favor use la versión de formación.
- ¡Vaya! Algo no ha ido como estaba previsto.\nDéjenos por todo en marcha de nuevo.
+ ¡Vaya! Algo no ha ido como estaba previsto.\nDéjenos poner todo en marcha de nuevo.Ir atrásCerrar appMás detalles
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 311ca3be72..765818b11e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -972,4 +972,5 @@
Sync ErrorMark for follow-upRemove
+ Coordinates
diff --git a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt
index e56dc950a1..b618e7c74c 100644
--- a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt
+++ b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/DataSetListViewModelTest.kt
@@ -102,6 +102,7 @@ class DataSetListViewModelTest {
"",
"",
"",
+ "",
State.SYNCED,
"",
true,
diff --git a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/mapper/DatasetCardMapperTest.kt b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/mapper/DatasetCardMapperTest.kt
index 51f353e37b..c8b802a156 100644
--- a/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/mapper/DatasetCardMapperTest.kt
+++ b/app/src/test/java/org/dhis2/usescases/datasets/datasetDetail/datasetlist/mapper/DatasetCardMapperTest.kt
@@ -40,6 +40,7 @@ class DatasetCardMapperTest {
"",
"",
"",
+ "",
"orgUnitName",
"nameCatCombo",
"Dataset PeriodName",
@@ -54,6 +55,7 @@ class DatasetCardMapperTest {
// When dataset is mapped to card item
val result = mapper.map(
dataset = datasetModel,
+ editable = true,
onSyncIconClick = {},
onCardCLick = {},
)
diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetailsTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetailsTest.kt
index c1040ea221..62c84f257c 100644
--- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetailsTest.kt
+++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetailsTest.kt
@@ -111,17 +111,22 @@ class ConfigureEventDetailsTest {
whenever(event.status()) doReturn EventStatus.ACTIVE
whenever(repository.getEditableStatus()) doReturn Editable()
+ // And event creation should be completed
+ val selectedDate = Date()
+ val selectedOrgUnit = ORG_UNIT_UID
+ val isCatComboCompleted = true
+
// When button is checked
val eventDetails = configureEventDetails.invoke(
- selectedDate = null,
- selectedOrgUnit = null,
+ selectedDate = selectedDate,
+ selectedOrgUnit = selectedOrgUnit,
catOptionComboUid = null,
- isCatComboCompleted = false,
+ isCatComboCompleted = isCatComboCompleted,
coordinates = null,
tempCreate = null,
).first()
- // Then action button should be invisible
+ // Then action button should be visible
assertTrue(eventDetails.isActionButtonVisible)
assert(eventDetails.actionButtonText.equals(UPDATE))
}
diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfiguratorTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfiguratorTest.kt
new file mode 100644
index 0000000000..88ef4cf2f4
--- /dev/null
+++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchPageConfiguratorTest.kt
@@ -0,0 +1,93 @@
+package org.dhis2.usescases.searchTrackEntity
+
+import com.google.common.truth.Truth.assertThat
+import org.hisp.dhis.android.core.common.FeatureType
+import org.hisp.dhis.android.core.program.Program
+import org.hisp.dhis.android.core.settings.AnalyticsDhisVisualizationsGroup
+import org.hisp.dhis.android.core.trackedentity.TrackedEntityType
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+
+class SearchPageConfiguratorTest {
+
+ private val searchRepository: SearchRepository = mock()
+ private val program: Program = mock()
+ private val teType: TrackedEntityType = mock()
+ private val visualizationGroup: AnalyticsDhisVisualizationsGroup = mock()
+
+ private val programUid: String = "programUid"
+ private val teTypeUid: String = "teTypeUid"
+
+ private lateinit var configurator: SearchPageConfigurator
+
+ @Before
+ fun setup() {
+ whenever(searchRepository.currentProgram()).doReturn(programUid)
+ whenever(searchRepository.getProgram(programUid)).doReturn(program)
+ whenever(program.featureType()).doReturn(FeatureType.POINT)
+ whenever(program.uid()).doReturn(programUid)
+ whenever(program.trackedEntityType()).doReturn(teType)
+ whenever(teType.uid()).doReturn(teTypeUid)
+
+ configurator = SearchPageConfigurator(searchRepository)
+ }
+
+ @Test
+ fun display_map_shortcut_to_false_if_no_program() {
+ whenever(searchRepository.currentProgram()).doReturn(null)
+
+ val hasCoordinates = configurator.programHasCoordinates()
+
+ assertThat(hasCoordinates).isFalse()
+
+ verify(searchRepository).currentProgram()
+ verifyNoMoreInteractions(searchRepository)
+ }
+
+ @Test
+ fun display_map_shortcut_to_true_if_program_has_coordinates() {
+ val hasCoordinates = configurator.programHasCoordinates()
+
+ assertThat(hasCoordinates).isTrue()
+
+ verify(searchRepository).currentProgram()
+ verify(searchRepository).getProgram(programUid)
+ verifyNoMoreInteractions(searchRepository)
+ }
+
+ @Test
+ fun display_map_do_not_shortcut_if_no_coordinates() {
+ whenever(program.featureType()).doReturn(FeatureType.NONE)
+
+ val hasCoordinates = configurator.programHasCoordinates()
+
+ assertThat(hasCoordinates).isFalse()
+
+ verify(searchRepository).currentProgram()
+ verify(searchRepository).getProgram(programUid)
+ verify(searchRepository).programStagesHaveCoordinates(programUid)
+ verify(searchRepository).programAttributesHaveCoordinates(programUid)
+ verify(searchRepository).trackedEntityType
+ verify(searchRepository).teTypeAttributesHaveCoordinates(any())
+ verify(searchRepository).eventsHaveCoordinates(programUid)
+ verifyNoMoreInteractions(searchRepository)
+ }
+
+ @Test
+ fun display_analytics_shortcut_to_false_if_no_program() {
+ whenever(searchRepository.currentProgram()).doReturn(null)
+
+ val hasAnalytics = configurator.programHasAnalytics()
+
+ assertThat(hasAnalytics).isFalse()
+
+ verify(searchRepository).currentProgram()
+ verifyNoMoreInteractions(searchRepository)
+ }
+}
diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt
index a674bce5fa..e007fe46dd 100644
--- a/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt
+++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/ui/mapper/TEIDetailMapperTest.kt
@@ -43,6 +43,7 @@ class TEIDetailMapperTest {
phoneCallback = {},
emailCallback = {},
programsCallback = {},
+ onImageClick = {},
)
diff --git a/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt
index 614900cdff..05e26bc37f 100644
--- a/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt
+++ b/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt
@@ -246,7 +246,7 @@ fun SearchTeiModel.setTeiImage(
teiImageView.setImageDrawable(null)
teiTextImageView.visibility = View.VISIBLE
val valueToShow = ArrayList(textAttributeValues.values)
- if (valueToShow[0] == null) {
+ if (valueToShow[0]?.value()?.isEmpty() != false) {
teiTextImageView.text = "?"
} else {
teiTextImageView.text = valueToShow[0].value()?.first().toString().toUpperCase()
diff --git a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java b/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java
index 242d3aba73..475f12f888 100644
--- a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java
+++ b/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java
@@ -14,6 +14,7 @@
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Objects;
public class SearchTeiModel implements CarouselItemModel {
@@ -170,7 +171,17 @@ public List getEnrollments() {
public List getProgramInfo() {
Collections.sort(programInfo, (program1, program2) -> program1.displayName().compareToIgnoreCase(program2.displayName()));
- return programInfo;
+ if (selectedEnrollment != null) {
+ List programs = new ArrayList<>();
+ for (Program program : programInfo) {
+ if (!Objects.equals(selectedEnrollment.program(), program.uid())) {
+ programs.add(program);
+ }
+ }
+ return programs;
+ } else {
+ return programInfo;
+ }
}
public void setOverdueDate(Date dateToShow) {
diff --git a/commons/src/main/java/org/dhis2/commons/featureconfig/data/FeatureConfigRepositoryImpl.kt b/commons/src/main/java/org/dhis2/commons/featureconfig/data/FeatureConfigRepositoryImpl.kt
index 5ec79ce3ab..617c3c4049 100644
--- a/commons/src/main/java/org/dhis2/commons/featureconfig/data/FeatureConfigRepositoryImpl.kt
+++ b/commons/src/main/java/org/dhis2/commons/featureconfig/data/FeatureConfigRepositoryImpl.kt
@@ -3,10 +3,14 @@ package org.dhis2.commons.featureconfig.data
import org.dhis2.commons.featureconfig.model.Feature
import org.dhis2.commons.featureconfig.model.FeatureState
import org.dhis2.commons.prefs.PreferenceProvider
+import org.hisp.dhis.android.core.D2
+import org.hisp.dhis.android.core.settings.ExperimentalFeature
import javax.inject.Inject
-class FeatureConfigRepositoryImpl @Inject constructor(val preferences: PreferenceProvider) :
- FeatureConfigRepository {
+class FeatureConfigRepositoryImpl @Inject constructor(
+ val preferences: PreferenceProvider,
+ val d2: D2,
+) : FeatureConfigRepository {
override val featuresList: List
get() = Feature.entries.map {
@@ -17,5 +21,12 @@ class FeatureConfigRepositoryImpl @Inject constructor(val preferences: Preferenc
preferences.setValue(featureState.feature.name, !featureState.enable)
}
- override fun isFeatureEnable(feature: Feature) = preferences.getBoolean(feature.name, false)
+ override fun isFeatureEnable(feature: Feature): Boolean {
+ return if (preferences.contains(feature.name)) {
+ preferences.getBoolean(feature.name, false)
+ } else {
+ d2.settingModule().generalSetting()
+ .hasExperimentalFeature(ExperimentalFeature.NewFormLayout).blockingGet()
+ }
+ }
}
diff --git a/commons/src/main/java/org/dhis2/commons/featureconfig/di/FeatureConfigModule.kt b/commons/src/main/java/org/dhis2/commons/featureconfig/di/FeatureConfigModule.kt
index 9293dce433..81059726b7 100644
--- a/commons/src/main/java/org/dhis2/commons/featureconfig/di/FeatureConfigModule.kt
+++ b/commons/src/main/java/org/dhis2/commons/featureconfig/di/FeatureConfigModule.kt
@@ -5,12 +5,15 @@ import dagger.Provides
import org.dhis2.commons.featureconfig.data.FeatureConfigRepository
import org.dhis2.commons.featureconfig.data.FeatureConfigRepositoryImpl
import org.dhis2.commons.prefs.PreferenceProvider
+import org.hisp.dhis.android.core.D2Manager
@Module
class FeatureConfigModule {
@Provides
fun provideRepository(preferenceProvider: PreferenceProvider): FeatureConfigRepository {
- return FeatureConfigRepositoryImpl(preferenceProvider)
+ return FeatureConfigRepositoryImpl(preferenceProvider, provideD2())
}
+
+ private fun provideD2() = D2Manager.getD2()
}
diff --git a/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModel.kt b/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModel.kt
index cf79ebb43b..6482b81f4a 100644
--- a/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModel.kt
+++ b/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModel.kt
@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import org.dhis2.commons.featureconfig.data.FeatureConfigRepository
import org.dhis2.commons.featureconfig.model.FeatureState
-class FeatureConfigViewModel constructor(
+class FeatureConfigViewModel(
private val repository: FeatureConfigRepository,
) : ViewModel() {
diff --git a/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListChipGroup.kt b/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListChipGroup.kt
index b5f3e5bba0..176adc638f 100644
--- a/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListChipGroup.kt
+++ b/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListChipGroup.kt
@@ -79,7 +79,11 @@ fun WorkingListChipGroup(
workingListViewModel: WorkingListViewModel,
) {
val workingListFilterState = workingListViewModel.workingListFilter.observeAsState()
- var selectedWorkingList by remember { mutableStateOf(null) }
+ var selectedWorkingList by remember {
+ mutableStateOf(
+ FilterManager.getInstance().currentWorkingList(),
+ )
+ }
workingListFilterState.value?.let { workingListFilter ->
LazyRow(modifier) {
@@ -95,15 +99,12 @@ fun WorkingListChipGroup(
),
label = workingList.label,
selected = selectedWorkingList == workingList,
- onSelected = { _ -> workingListFilter.onChecked(workingList.id()) },
+ onSelected = { _ ->
+ workingListFilter.onChecked(workingList.id())
+ selectedWorkingList = FilterManager.getInstance().currentWorkingList()
+ },
)
}
}
- workingListFilter.observeScope()
- .addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
- override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
- selectedWorkingList = FilterManager.getInstance().currentWorkingList()
- }
- })
}
}
diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/DataTable.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/DataTable.kt
index fdc6519d60..365b5ca459 100644
--- a/compose-table/src/main/java/org/dhis2/composetable/ui/DataTable.kt
+++ b/compose-table/src/main/java/org/dhis2/composetable/ui/DataTable.kt
@@ -28,13 +28,14 @@ fun DataTable(tableList: List, bottomContent: @Composable (() -> Uni
Table(
tableList = tableList,
tableHeaderRow = { index, tableModel ->
+ val isSingleValue = tableModel.tableRows.firstOrNull()?.values?.size == 1
TableHeaderRow(
modifier = Modifier
.background(Color.White),
cornerUiState = TableCornerUiState(
isSelected = tableSelection.isCornerSelected(tableModel.id ?: ""),
onTableResize = {
- if (tableModel.tableRows.first().values.size == 1) {
+ if (isSingleValue) {
tableResizeActions.onRowHeaderResize(
tableModel.id ?: "",
it,
@@ -47,7 +48,7 @@ fun DataTable(tableList: List, bottomContent: @Composable (() -> Uni
}
},
onResizing = { resizingCell = it },
- singleValueTable = tableModel.tableRows.first().values.size == 1,
+ singleValueTable = isSingleValue,
),
tableModel = tableModel,
horizontalScrollState = horizontalScrollStates[index],
diff --git a/dhis2_android_maps/build.gradle.kts b/dhis2_android_maps/build.gradle.kts
index df3e71b938..9f56ae1cc9 100644
--- a/dhis2_android_maps/build.gradle.kts
+++ b/dhis2_android_maps/build.gradle.kts
@@ -35,6 +35,7 @@ android {
}
buildFeatures {
+ compose = true
dataBinding = true
}
@@ -43,6 +44,10 @@ android {
kotlinOptions {
jvmTarget = "17"
}
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get()
+ }
}
dependencies {
diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt
deleted file mode 100644
index 7adeb777b7..0000000000
--- a/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.dhis2.maps.geometry.point
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.databinding.DataBindingUtil
-import androidx.recyclerview.widget.RecyclerView
-import org.dhis2.maps.R
-import org.dhis2.maps.databinding.ItemPointGeoBinding
-
-class PointAdapter(
- val viewModel: PointViewModel,
-) : RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
- val binding: ItemPointGeoBinding = DataBindingUtil.inflate(
- LayoutInflater.from(parent.context),
- R.layout.item_point_geo,
- parent,
- false,
- )
- return Holder(binding)
- }
-
- override fun getItemCount(): Int {
- return 1
- }
-
- override fun onBindViewHolder(holder: Holder, position: Int) {
- holder.bind()
- }
-
- inner class Holder(val binding: ItemPointGeoBinding) : RecyclerView.ViewHolder(binding.root) {
- fun bind() {
- binding.viewModel = viewModel
- }
- }
-}
diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt
index 83b2b391db..bfeeb6bf57 100644
--- a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt
+++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
+import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
@@ -14,7 +15,6 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
import com.mapbox.geojson.Feature
import com.mapbox.geojson.Point
import com.mapbox.geojson.Polygon
@@ -35,13 +35,11 @@ import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
import org.dhis2.commons.extensions.truncate
import org.dhis2.maps.R
import org.dhis2.maps.camera.initCameraToViewAllElements
-import org.dhis2.maps.camera.moveCameraToDevicePosition
import org.dhis2.maps.camera.moveCameraToPosition
import org.dhis2.maps.databinding.ActivityMapSelectorBinding
import org.dhis2.maps.extensions.polygonToLatLngBounds
import org.dhis2.maps.extensions.toLatLng
import org.dhis2.maps.geometry.bound.GetBoundingBox
-import org.dhis2.maps.geometry.point.PointAdapter
import org.dhis2.maps.geometry.point.PointViewModel
import org.dhis2.maps.geometry.polygon.PolygonAdapter
import org.dhis2.maps.geometry.polygon.PolygonViewModel
@@ -50,6 +48,8 @@ import org.dhis2.maps.location.MapActivityLocationCallback
import org.hisp.dhis.android.core.arch.helpers.GeometryHelper
import org.hisp.dhis.android.core.common.FeatureType
import org.hisp.dhis.android.core.common.Geometry
+import org.hisp.dhis.mobile.ui.designsystem.component.Button
+import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle
class MapSelectorActivity :
AppCompatActivity(),
@@ -60,6 +60,14 @@ class MapSelectorActivity :
init = true
if (initialCoordinates == null) {
map.moveCameraToPosition(latLng)
+ getPointViewModel()?.let {
+ val point =
+ Point.fromLngLat(
+ latLng.longitude.truncate(),
+ latLng.latitude.truncate(),
+ )
+ setPointToViewModel(point, it)
+ }
}
}
}
@@ -79,6 +87,10 @@ class MapSelectorActivity :
BaseMapManager(this, emptyList())
}
+ private var onSaveButtonClick: (() -> Unit)? = null
+
+ private var pointViewModel: PointViewModel? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_map_selector)
@@ -112,19 +124,29 @@ class MapSelectorActivity :
binding.mapPositionButton.setOnClickListener {
centerCameraOnMyPosition()
}
+
+ binding.saveButton.setContent {
+ Button(
+ style = ButtonStyle.FILLED,
+ text = resources.getString(R.string.done),
+ onClick = { onSaveButtonClick?.invoke() },
+ )
+ }
}
private fun centerCameraOnMyPosition() {
- val isLocationActivated =
- map.locationComponent.isLocationComponentActivated
- if (isLocationActivated) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ val isLocationActivated = map.locationComponent.isLocationComponentActivated
val isLocationEnabled = map.locationComponent.isLocationComponentEnabled
- if (isLocationEnabled) {
+ if (isLocationActivated && isLocationEnabled) {
map.locationComponent.lastKnownLocation?.let {
val latLong = LatLng(it)
- map.moveCameraToDevicePosition(latLong)
- if (locationType == FeatureType.POINT) {
- val viewModel = ViewModelProvider(this)[PointViewModel::class.java]
+ map.moveCameraToPosition(latLong)
+ getPointViewModel()?.let { viewModel ->
val point =
Point.fromLngLat(
latLong.longitude.truncate(),
@@ -133,7 +155,19 @@ class MapSelectorActivity :
setPointToViewModel(point, viewModel)
}
}
+ } else {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ CENTER_MY_POSITION,
+ )
}
+ } else {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ CENTER_MY_POSITION,
+ )
}
}
@@ -160,31 +194,38 @@ class MapSelectorActivity :
}
}
- private fun bindPoint(initialCoordinates: String?) {
- val viewModel = ViewModelProvider(this)[PointViewModel::class.java]
- binding.recycler.layoutManager = LinearLayoutManager(this)
- binding.recycler.adapter = PointAdapter(viewModel)
- map.addOnMapClickListener {
- val point = Point.fromLngLat(it.longitude.truncate(), it.latitude.truncate())
- setPointToViewModel(point, viewModel)
- true
- }
- binding.saveButton.setOnClickListener {
- val value = viewModel.getPointAsString()
- value?.let {
- finishResult(it)
- }
+ private fun getPointViewModel(): PointViewModel? {
+ if (locationType == FeatureType.POINT && pointViewModel == null) {
+ pointViewModel = ViewModelProvider(this)[PointViewModel::class.java]
}
+ return pointViewModel
+ }
- if (initialCoordinates != null) {
- val initGeometry =
- Geometry.builder().coordinates(initialCoordinates).type(locationType).build()
- val pointGeometry = GeometryHelper.getPoint(initGeometry)
- pointGeometry.let { sdkPoint ->
- val point = Point.fromLngLat(sdkPoint[0], sdkPoint[1])
+ private fun bindPoint(initialCoordinates: String?) {
+ getPointViewModel()?.let { viewModel ->
+ binding.recycler.visibility = View.GONE
+ map.addOnMapClickListener {
+ val point = Point.fromLngLat(it.longitude.truncate(), it.latitude.truncate())
setPointToViewModel(point, viewModel)
+ true
+ }
+ onSaveButtonClick = {
+ val value = viewModel.getPointAsString()
+ value?.let {
+ finishResult(it)
+ }
+ }
+
+ if (initialCoordinates != null) {
+ val initGeometry =
+ Geometry.builder().coordinates(initialCoordinates).type(locationType).build()
+ val pointGeometry = GeometryHelper.getPoint(initGeometry)
+ pointGeometry.let { sdkPoint ->
+ val point = Point.fromLngLat(sdkPoint[0], sdkPoint[1])
+ setPointToViewModel(point, viewModel)
+ }
+ map.moveCameraToPosition(pointGeometry.toLatLng())
}
- map.moveCameraToPosition(pointGeometry.toLatLng())
}
}
@@ -275,7 +316,7 @@ class MapSelectorActivity :
viewModel.add(polygonPoint)
true
}
- binding.saveButton.setOnClickListener {
+ onSaveButtonClick = {
val value = viewModel.getPointAsString()
value?.let {
finishResult(it)
@@ -415,11 +456,21 @@ class MapSelectorActivity :
centerMapOnCurrentLocation()
}
}
+
+ CENTER_MY_POSITION -> {
+ if (grantResults.isNotEmpty() &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
+ ) {
+ enableLocationComponent()
+ centerCameraOnMyPosition()
+ }
+ }
}
}
companion object {
private const val ACCESS_LOCATION_PERMISSION_REQUEST = 102
+ private const val CENTER_MY_POSITION = 103
const val DATA_EXTRA = "data_extra"
const val LOCATION_TYPE_EXTRA = "LOCATION_TYPE_EXTRA"
const val INITIAL_GEOMETRY_COORDINATES = "INITIAL_DATA"
diff --git a/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml b/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml
index 8b414e0808..94353f4f54 100644
--- a/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml
+++ b/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml
@@ -38,24 +38,6 @@
android:text="@string/select_location"
android:textSize="20sp"
app:layout_constraintStart_toEndOf="@id/back" />
-
-
-
-
-
-
-
+ app:layout_constraintTop_toBottomOf="@id/mapContainer">
-
diff --git a/dhis2_android_maps/src/main/res/layout/item_point_geo.xml b/dhis2_android_maps/src/main/res/layout/item_point_geo.xml
deleted file mode 100644
index db19523580..0000000000
--- a/dhis2_android_maps/src/main/res/layout/item_point_geo.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/form/src/main/java/org/dhis2/form/data/FormRepository.kt b/form/src/main/java/org/dhis2/form/data/FormRepository.kt
index ef2f522f91..cddf628b63 100644
--- a/form/src/main/java/org/dhis2/form/data/FormRepository.kt
+++ b/form/src/main/java/org/dhis2/form/data/FormRepository.kt
@@ -22,6 +22,7 @@ interface FormRepository {
fun updateSectionOpened(action: RowAction)
fun removeAllValues()
fun setFieldRequestingCoordinates(uid: String, requestInProcess: Boolean)
+ fun setFieldAddingImage(uid: String, requestInProcess: Boolean)
fun clearFocusItem()
fun storeFile(id: String, filePath: String?): StoreResult?
fun areSectionCollapsable(): Boolean
diff --git a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt
index bcdd937a56..4d52a7041e 100644
--- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt
+++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt
@@ -403,6 +403,19 @@ class FormRepositoryImpl(
}
}
+ override fun setFieldAddingImage(uid: String, requestInProcess: Boolean) {
+ itemList.let { list ->
+ list.find { item ->
+ item.uid == uid
+ }?.let { item ->
+ itemList = list.updated(
+ list.indexOf(item),
+ item.setIsLoadingData(requestInProcess),
+ )
+ }
+ }
+ }
+
private fun List.mergeListWithErrorFields(
fieldsWithError: MutableList,
): List {
diff --git a/form/src/main/java/org/dhis2/form/model/ActionType.kt b/form/src/main/java/org/dhis2/form/model/ActionType.kt
index 47f288cdf4..03e8f24f94 100644
--- a/form/src/main/java/org/dhis2/form/model/ActionType.kt
+++ b/form/src/main/java/org/dhis2/form/model/ActionType.kt
@@ -9,6 +9,7 @@ enum class ActionType {
ON_CLEAR,
ON_FINISH,
ON_REQUEST_COORDINATES,
- ON_CANCELL_REQUEST_COORDINATES,
+ ON_CANCEL_REQUEST_COORDINATES,
ON_STORE_FILE,
+ ON_ADD_IMAGE_FINISHED,
}
diff --git a/form/src/main/java/org/dhis2/form/model/UiRenderType.kt b/form/src/main/java/org/dhis2/form/model/UiRenderType.kt
index 32d91d06ab..91574aa2b8 100644
--- a/form/src/main/java/org/dhis2/form/model/UiRenderType.kt
+++ b/form/src/main/java/org/dhis2/form/model/UiRenderType.kt
@@ -14,6 +14,7 @@ enum class UiRenderType {
SEQUENCIAL,
QR_CODE,
BAR_CODE,
+ GS1_DATAMATRIX,
CANVAS,
TOGGLE,
;
diff --git a/form/src/main/java/org/dhis2/form/ui/DataEntryAdapter.kt b/form/src/main/java/org/dhis2/form/ui/DataEntryAdapter.kt
index 9fae8dde05..4c6b3ff553 100644
--- a/form/src/main/java/org/dhis2/form/ui/DataEntryAdapter.kt
+++ b/form/src/main/java/org/dhis2/form/ui/DataEntryAdapter.kt
@@ -79,7 +79,7 @@ class DataEntryAdapter(
fun updateSectionData(position: Int, isHeader: Boolean) {
(getItem(position) as SectionUiModelImpl?)!!.setShowBottomShadow(
- collapsableSections &&
+ !collapsableSections &&
!isHeader && position > 0 && getItem(
position - 1,
) !is SectionUiModelImpl,
diff --git a/form/src/main/java/org/dhis2/form/ui/Form.kt b/form/src/main/java/org/dhis2/form/ui/Form.kt
index fb4cd53d5f..9f5ac6d8d3 100644
--- a/form/src/main/java/org/dhis2/form/ui/Form.kt
+++ b/form/src/main/java/org/dhis2/form/ui/Form.kt
@@ -1,24 +1,26 @@
package org.dhis2.form.ui
-import android.text.TextWatcher
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -29,6 +31,7 @@ import org.dhis2.form.ui.event.RecyclerViewUiEvents
import org.dhis2.form.ui.intent.FormIntent
import org.dhis2.form.ui.provider.inputfield.FieldProvider
import org.hisp.dhis.mobile.ui.designsystem.component.Section
+import org.hisp.dhis.mobile.ui.designsystem.component.SectionState
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -36,15 +39,11 @@ fun Form(
sections: List = emptyList(),
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
- textWatcher: TextWatcher,
- coordinateTextWatcher: LatitudeLongitudeTextWatcher,
- needToForceUpdate: Boolean,
resources: ResourceManager,
) {
val scrollState = rememberLazyListState()
- val context = LocalContext.current
- val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
+ val scope = rememberCoroutineScope()
val callback = remember {
object : FieldUiModel.Callback {
override fun intent(intent: FormIntent) {
@@ -60,15 +59,40 @@ fun Form(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
- .padding(horizontal = 16.dp),
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = { focusManager.clearFocus() },
+ ),
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
state = scrollState,
- verticalArrangement = spacedBy(24.dp),
) {
if (sections.isNotEmpty()) {
this.itemsIndexed(
items = sections,
key = { _, fieldUiModel -> fieldUiModel.uid },
) { _, section ->
+ val isSectionOpen = remember(section.state) {
+ derivedStateOf { section.state == SectionState.OPEN }
+ }
+ LaunchedEffect(isSectionOpen.value) {
+ if (isSectionOpen.value) {
+ scrollState.animateScrollToItem(sections.indexOf(section))
+ focusManager.moveFocus(FocusDirection.Next)
+ }
+ }
+
+ val onNextSection: () -> Unit = {
+ getNextSection(section, sections)?.let {
+ intentHandler.invoke(FormIntent.OnSection(it.uid))
+ scope.launch {
+ scrollState.animateScrollToItem(sections.indexOf(it))
+ }
+ } ?: run {
+ focusManager.clearFocus()
+ }
+ }
+
Section(
title = section.title,
isLastSection = getNextSection(section, sections) == null,
@@ -78,22 +102,12 @@ fun Form(
state = section.state,
errorCount = section.errorCount(),
warningCount = section.warningCount(),
- onNextSection = {
- getNextSection(section, sections)?.let {
- coroutineScope.launch {
- scrollState.animateScrollToItem(sections.indexOf(it))
- intentHandler.invoke(FormIntent.OnSection(it.uid))
- }
- }
- },
+ onNextSection = onNextSection,
onSectionClick = {
- coroutineScope.launch {
- scrollState.animateScrollToItem(sections.indexOf(section))
- intentHandler.invoke(FormIntent.OnSection(section.uid))
- }
+ intentHandler.invoke(FormIntent.OnSection(section.uid))
},
content = {
- section.fields.forEach { fieldUiModel ->
+ section.fields.forEachIndexed { index, fieldUiModel ->
fieldUiModel.setCallback(callback)
FieldProvider(
modifier = Modifier.animateItemPlacement(
@@ -102,17 +116,19 @@ fun Form(
easing = LinearOutSlowInEasing,
),
),
- context = context,
fieldUiModel = fieldUiModel,
- needToForceUpdate = needToForceUpdate,
- textWatcher = textWatcher,
- coordinateTextWatcher = coordinateTextWatcher,
uiEventHandler = uiEventHandler,
intentHandler = intentHandler,
resources = resources,
focusManager = focusManager,
+ onNextClicked = {
+ if (index == section.fields.size - 1) {
+ onNextSection()
+ } else {
+ focusManager.moveFocus(FocusDirection.Down)
+ }
+ },
)
- Spacer(modifier = Modifier.height(24.dp))
}
},
)
diff --git a/form/src/main/java/org/dhis2/form/ui/FormView.kt b/form/src/main/java/org/dhis2/form/ui/FormView.kt
index b607544f06..3e421483ef 100644
--- a/form/src/main/java/org/dhis2/form/ui/FormView.kt
+++ b/form/src/main/java/org/dhis2/form/ui/FormView.kt
@@ -131,11 +131,6 @@ class FormView : Fragment() {
}
}
- private val coordinateTextWatcher = LatitudeLongitudeTextWatcher { coordinates ->
- viewModel.items.value?.find { it.focused && it.valueType == ValueType.COORDINATE }
- ?.onTextChange(coordinates)
- }
-
private val qrScanContent = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { qrData ->
val intent = FormIntent.OnSave(
@@ -207,24 +202,46 @@ class FormView : Fragment() {
TEMP_FILE,
).rotateImage(requireContext())
onSavePicture?.invoke(imageFile.path)
+
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
+ } else {
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
}
}
private val pickImage =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == RESULT_OK) {
- getFileFromGallery(requireContext(), it.data?.data)?.also { file ->
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
+ if (activityResult.resultCode == RESULT_OK) {
+ getFileFromGallery(requireContext(), activityResult.data?.data)?.also { file ->
onSavePicture?.invoke(file.path)
}
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
+ } else {
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
}
}
private val pickFile =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
- uri?.let {
+ if (uri != null) {
getFileFrom(requireContext(), uri)?.also { file ->
onSavePicture?.invoke(file.path)
}
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
+ } else {
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
}
}
@@ -348,9 +365,6 @@ class FormView : Fragment() {
sections = sections,
intentHandler = ::intentHandler,
uiEventHandler = ::uiEventHandler,
- textWatcher = textWatcher,
- coordinateTextWatcher = coordinateTextWatcher,
- needToForceUpdate = needToForceUpdate,
resources = Injector.provideResourcesManager(context),
)
}
@@ -907,6 +921,11 @@ class FormView : Fragment() {
)
MaterialAlertDialogBuilder(requireActivity(), R.style.MaterialDialog)
.setTitle(requireContext().getString(R.string.select_option))
+ .setOnCancelListener {
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
+ }
.setItems(options) { dialog: DialogInterface, item: Int ->
run {
when (options[item]) {
@@ -927,6 +946,11 @@ class FormView : Fragment() {
requireContext().getString(R.string.from_gallery) -> {
pickImage.launch(Intent(Intent.ACTION_PICK).apply { type = "image/*" })
}
+ requireContext().getString(R.string.cancel) -> {
+ viewModel.getFocusedItemUid()?.let {
+ viewModel.submitIntent(FormIntent.OnAddImageFinished(it))
+ }
+ }
}
dialog.dismiss()
}
@@ -953,7 +977,7 @@ class FormView : Fragment() {
}
private fun openFile(event: RecyclerViewUiEvents.OpenFile) {
- event.field.value?.let { filePath ->
+ event.field.displayName?.let { filePath ->
val file = File(filePath)
val fileUri = FileProvider.getUriForFile(
requireContext(),
diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt
index 9e150beeac..3993f0e7de 100644
--- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt
+++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt
@@ -172,6 +172,7 @@ class FormViewModel(
if (action.valueType == ValueType.COORDINATE) {
repository.setFieldRequestingCoordinates(action.id, false)
}
+
repository.updateErrorList(action)
if (action.error != null) {
StoreResult(
@@ -242,7 +243,7 @@ class FormViewModel(
)
}
- ActionType.ON_CANCELL_REQUEST_COORDINATES -> {
+ ActionType.ON_CANCEL_REQUEST_COORDINATES -> {
repository.setFieldRequestingCoordinates(action.id, false)
StoreResult(
action.id,
@@ -250,6 +251,14 @@ class FormViewModel(
)
}
+ ActionType.ON_ADD_IMAGE_FINISHED -> {
+ repository.setFieldAddingImage(action.id, false)
+ StoreResult(
+ action.id,
+ ValueStoreResult.VALUE_HAS_NOT_CHANGED,
+ )
+ }
+
ActionType.ON_STORE_FILE -> {
val saveResult = repository.storeFile(action.id, action.value)
when (saveResult?.valueStoreResult) {
@@ -279,7 +288,6 @@ class FormViewModel(
private fun saveLastFocusedItem(rowAction: RowAction) = getLastFocusedTextItem()?.let {
val error = checkFieldError(it.valueType, it.value, it.fieldMask)
if (error != null) {
- // save autocomplete form here
val action = rowActionFromIntent(
FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask),
)
@@ -302,16 +310,17 @@ class FormViewModel(
ValueStoreResult.VALUE_HAS_NOT_CHANGED,
)
- private fun checkAutoCompleteForLastFocusedItem(fieldUidModel: FieldUiModel) = getLastFocusedTextItem()?.let {
- if (fieldUidModel.renderingType == UiRenderType.AUTOCOMPLETE && fieldUidModel.value != null) {
- val autoCompleteValues =
- getListFromPreference(fieldUidModel.uid)
- if (!autoCompleteValues.contains(fieldUidModel.value)) {
- autoCompleteValues.add(fieldUidModel.value.toString())
- saveListToPreference(fieldUidModel.uid, autoCompleteValues)
+ private fun checkAutoCompleteForLastFocusedItem(fieldUidModel: FieldUiModel) =
+ getLastFocusedTextItem()?.let {
+ if (fieldUidModel.renderingType == UiRenderType.AUTOCOMPLETE && !fieldUidModel.value.isNullOrEmpty() && fieldUidModel.value?.trim()?.length != 0) {
+ val autoCompleteValues =
+ getListFromPreference(fieldUidModel.uid)
+ if (!autoCompleteValues.contains(fieldUidModel.value)) {
+ autoCompleteValues.add(fieldUidModel.value.toString())
+ saveListToPreference(fieldUidModel.uid, autoCompleteValues)
+ }
}
}
- }
fun valueTypeIsTextField(valueType: ValueType?, renderType: UiRenderType? = null): Boolean {
return if (valueType == null) {
@@ -446,7 +455,14 @@ class FormViewModel(
createRowAction(
uid = intent.uid,
value = null,
- actionType = ActionType.ON_CANCELL_REQUEST_COORDINATES,
+ actionType = ActionType.ON_CANCEL_REQUEST_COORDINATES,
+ )
+
+ is FormIntent.OnAddImageFinished ->
+ createRowAction(
+ uid = intent.uid,
+ value = null,
+ actionType = ActionType.ON_ADD_IMAGE_FINISHED,
)
is FormIntent.OnStoreFile ->
@@ -456,13 +472,29 @@ class FormViewModel(
actionType = ActionType.ON_STORE_FILE,
valueType = intent.valueType,
)
+
+ is FormIntent.OnSaveDate -> {
+ val error = checkFieldError(
+ valueType = intent.valueType,
+ fieldValue = intent.value,
+ allowFutureDates = intent.allowFutureDates,
+ )
+
+ createRowAction(
+ uid = intent.uid,
+ value = intent.value,
+ error = error,
+ valueType = intent.valueType,
+ )
+ }
}
}
private fun checkFieldError(
valueType: ValueType?,
fieldValue: String?,
- fieldMask: String?,
+ fieldMask: String? = null,
+ allowFutureDates: Boolean? = null,
): Throwable? {
if (fieldValue.isNullOrEmpty()) {
return null
@@ -471,18 +503,19 @@ class FormViewModel(
return fieldValue.let { value ->
val result = when (valueType) {
ValueType.DATE -> {
- validateDateFormats(fieldValue, valueType)
+ validateDateFormats(fieldValue, valueType, allowFutureDates)
}
+
ValueType.TIME -> {
validateTimeFormat(fieldValue, valueType)
}
ValueType.DATETIME -> {
- validateDateTimeFormat(fieldValue, valueType)
+ validateDateTimeFormat(fieldValue, valueType, allowFutureDates)
}
ValueType.AGE -> {
- validateDateFormats(fieldValue, valueType)
+ validateDateFormats(fieldValue, valueType, allowFutureDates)
}
else -> {
@@ -507,6 +540,7 @@ class FormViewModel(
private fun validateDateTimeFormat(
dateTimeString: String,
valueType: ValueType,
+ allowFutureDates: Boolean?,
): Result {
val regex = Regex("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$")
@@ -516,11 +550,14 @@ class FormViewModel(
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
- return try {
- LocalDateTime.parse(dateTimeString, formatter)
- valueType.validator.validate(dateTimeString)
+ try {
+ val date = LocalDateTime.parse(dateTimeString, formatter)
+ if (allowFutureDates == false && date.isAfter(LocalDateTime.now())) {
+ return Result.Failure(DateFailure.ParseException)
+ }
+ return valueType.validator.validate(dateTimeString)
} catch (e: DateTimeParseException) {
- Result.Failure(DateTimeFailure.ParseException)
+ return Result.Failure(DateTimeFailure.ParseException)
}
}
@@ -539,14 +576,18 @@ class FormViewModel(
private fun validateDateFormats(
dateString: String,
valueType: ValueType,
+ allowFutureDates: Boolean?,
): Result {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
- return try {
- LocalDate.parse(dateString, formatter)
- valueType.validator.validate(dateString)
+ try {
+ val date = LocalDate.parse(dateString, formatter)
+ if (allowFutureDates == false && date.isAfter(LocalDate.now())) {
+ return Result.Failure(DateFailure.ParseException)
+ }
+ return valueType.validator.validate(dateString)
} catch (e: DateTimeParseException) {
- Result.Failure(DateFailure.ParseException)
+ return Result.Failure(DateFailure.ParseException)
}
}
@@ -599,7 +640,7 @@ class FormViewModel(
}
fun getFocusedItemUid(): String? {
- return items.value?.first { it.focused }?.uid
+ return items.value?.firstOrNull { it.focused }?.uid
}
private fun processCalculatedItems(skipProgramRules: Boolean = false, finish: Boolean = false) {
@@ -719,6 +760,7 @@ class FormViewModel(
val type = object : TypeToken>() {}.type
return gson.fromJson(json, type)
}
+
private fun saveListToPreference(uid: String, list: List) {
val gson = Gson()
val json = gson.toJson(list)
diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt b/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt
index bf35e6884b..1bbed7d93c 100644
--- a/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt
+++ b/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt
@@ -42,6 +42,7 @@ import org.dhis2.form.data.FormFileProvider
import org.dhis2.form.databinding.QrDetailDialogBinding
import org.dhis2.form.model.UiRenderType
import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper
+import org.hisp.dhis.lib.expression.math.GS1Elements
import org.hisp.dhis.mobile.ui.designsystem.component.BarcodeBlock
import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell
import org.hisp.dhis.mobile.ui.designsystem.component.ButtonCarousel
@@ -197,7 +198,7 @@ QRDetailBottomDialog(
val buttonList = getComposeButtonList()
BottomSheetShell(
modifier = modifier,
- title = if (renderingType == UiRenderType.QR_CODE) resources.getString(R.string.qr_code) else resources.getString(R.string.bar_code),
+ title = if (renderingType == UiRenderType.QR_CODE || renderingType == UiRenderType.GS1_DATAMATRIX) resources.getString(R.string.qr_code) else resources.getString(R.string.bar_code),
icon = {
Icon(
imageVector = Icons.Outlined.Info,
@@ -207,10 +208,15 @@ QRDetailBottomDialog(
},
content = {
Row(horizontalArrangement = Arrangement.Center) {
- if (renderingType == UiRenderType.QR_CODE) {
- QrCodeBlock(data = value)
- } else {
- BarcodeBlock(data = value)
+ when (renderingType) {
+ UiRenderType.QR_CODE, UiRenderType.GS1_DATAMATRIX -> {
+ val isGS1Matrix = value.startsWith(GS1Elements.GS1_d2_IDENTIFIER.element)
+ val content = formattedContent(value)
+ QrCodeBlock(data = content, isDataMatrix = isGS1Matrix)
+ }
+ else -> {
+ BarcodeBlock(data = value)
+ }
}
}
},
@@ -225,6 +231,10 @@ QRDetailBottomDialog(
}
}
+ private fun formattedContent(value: String) =
+ value.removePrefix(GS1Elements.GS1_d2_IDENTIFIER.element)
+ .removePrefix(GS1Elements.GS1_GROUP_SEPARATOR.element)
+
private fun getComposeButtonList(): List {
val scanItem = CarouselButtonData(
icon = {
diff --git a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt
index 3c2c7e9c40..583b9e2b27 100644
--- a/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt
+++ b/form/src/main/java/org/dhis2/form/ui/intent/FormIntent.kt
@@ -76,4 +76,15 @@ sealed class FormIntent : MviIntent {
data class OnCancelRequestCoordinates(
val uid: String,
) : FormIntent()
+
+ data class OnAddImageFinished(
+ val uid: String,
+ ) : FormIntent()
+
+ data class OnSaveDate(
+ val uid: String,
+ val value: String?,
+ val valueType: ValueType?,
+ val allowFutureDates: Boolean = true,
+ ) : FormIntent()
}
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt b/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt
index f80ff3da64..3ae3231f96 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt
@@ -28,6 +28,8 @@ class UiEventTypesProviderImpl : UiEventTypesProvider {
UiRenderType.AUTOCOMPLETE
ValueTypeRenderingType.QR_CODE ->
UiRenderType.QR_CODE
+ ValueTypeRenderingType.GS1_DATAMATRIX ->
+ UiRenderType.GS1_DATAMATRIX
ValueTypeRenderingType.BAR_CODE ->
UiRenderType.BAR_CODE
ValueTypeRenderingType.CANVAS ->
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt
index 635afa39b3..06d552786f 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt
@@ -32,7 +32,7 @@ fun ProvideInputAge(
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
resources: ResourceManager,
) {
- var inputType by remember {
+ var inputType by remember(fieldUiModel.value) {
mutableStateOf(
if (!fieldUiModel.value.isNullOrEmpty()) {
formatStoredDateToUI(fieldUiModel.value!!)?.let {
@@ -110,6 +110,7 @@ fun ProvideInputAge(
fieldUiModel.uid,
formatUIDateToStored(type.value),
fieldUiModel.valueType,
+ fieldUiModel.allowFutureDates,
)
}
}
@@ -120,6 +121,7 @@ fun ProvideInputAge(
fieldUiModel.uid,
null,
fieldUiModel.valueType,
+ fieldUiModel.allowFutureDates,
)
}
}
@@ -132,12 +134,14 @@ private fun saveValue(
uid: String,
value: String?,
valueType: ValueType?,
+ allowFutureDates: Boolean?,
) {
intentHandler.invoke(
- FormIntent.OnSave(
+ FormIntent.OnSaveDate(
uid,
value,
valueType,
+ allowFutureDates ?: false,
),
)
}
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt
index e2011611e5..59fe4cc758 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CheckBoxProvider.kt
@@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.form.R
import org.dhis2.form.extensions.inputState
@@ -19,6 +20,7 @@ internal fun ProvideCheckBoxInput(
modifier: Modifier,
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
+ focusRequester: FocusRequester,
) {
val data = fieldUiModel.optionSetConfiguration?.optionsToDisplay()?.map { option ->
CheckBoxData(
@@ -39,7 +41,7 @@ internal fun ProvideCheckBoxInput(
legendData = fieldUiModel.legend(),
isRequired = fieldUiModel.mandatory,
onItemChange = { item ->
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
intentHandler(
FormIntent.OnSave(
fieldUiModel.uid,
@@ -50,7 +52,7 @@ internal fun ProvideCheckBoxInput(
)
},
onClearSelection = {
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
intentHandler(
FormIntent.ClearValue(fieldUiModel.uid),
)
@@ -64,6 +66,7 @@ internal fun ProvideYesNoCheckBoxInput(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
resources: ResourceManager,
+ focusRequester: FocusRequester,
) {
val data = listOf(
CheckBoxData(
@@ -90,7 +93,7 @@ internal fun ProvideYesNoCheckBoxInput(
legendData = fieldUiModel.legend(),
isRequired = fieldUiModel.mandatory,
onItemChange = { item ->
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
when (item.uid) {
"true" -> {
intentHandler(
@@ -116,7 +119,7 @@ internal fun ProvideYesNoCheckBoxInput(
}
},
onClearSelection = {
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
intentHandler(
FormIntent.ClearValue(fieldUiModel.uid),
)
@@ -129,6 +132,7 @@ internal fun ProvideYesOnlyCheckBoxInput(
modifier: Modifier,
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
+ focusRequester: FocusRequester,
) {
val cbData = CheckBoxData(
uid = "",
@@ -145,7 +149,7 @@ internal fun ProvideYesOnlyCheckBoxInput(
legendData = fieldUiModel.legend(),
isRequired = fieldUiModel.mandatory,
onClick = {
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
if (!fieldUiModel.isAffirmativeChecked) {
intentHandler(
FormIntent.OnSave(
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CoordinatesProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CoordinatesProvider.kt
index 06593a32fd..854ae703e4 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CoordinatesProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/CoordinatesProvider.kt
@@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.form.R
import org.dhis2.form.extensions.inputState
@@ -43,6 +44,7 @@ fun ProvideInputCoordinate(
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
resources: ResourceManager,
+ focusRequester: FocusRequester,
) {
InputCoordinate(
title = fieldUiModel.label,
@@ -56,6 +58,7 @@ fun ProvideInputCoordinate(
isRequired = fieldUiModel.mandatory,
modifier = modifier,
onResetButtonClicked = {
+ focusRequester.requestFocus()
intentHandler.invoke(
FormIntent.OnSave(
uid = fieldUiModel.uid,
@@ -65,6 +68,7 @@ fun ProvideInputCoordinate(
)
},
onUpdateButtonClicked = {
+ focusRequester.requestFocus()
uiEventHandler.invoke(
RecyclerViewUiEvents.RequestLocationByMap(
uid = fieldUiModel.uid,
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt
index 93cffa9632..0af17b3da0 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/DateProvider.kt
@@ -27,6 +27,7 @@ fun ProvideInputDate(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
+ onNextClicked: () -> Unit,
) {
val (actionType, visualTransformation) = when (fieldUiModel.valueType) {
ValueType.DATETIME -> DateTimeActionIconType.DATE_TIME to DateTimeTransformation()
@@ -85,14 +86,16 @@ fun ProvideInputDate(
isRequired = fieldUiModel.mandatory,
visualTransformation = visualTransformation,
onFocusChanged = {},
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
if (isValid(it, fieldUiModel.valueType)) {
intentHandler.invoke(
- FormIntent.OnSave(
+ FormIntent.OnSaveDate(
uid = fieldUiModel.uid,
value = formatUIDateToStored(it, fieldUiModel.valueType),
valueType = fieldUiModel.valueType,
+ allowFutureDates = fieldUiModel.allowFutureDates ?: true,
),
)
}
@@ -197,21 +200,25 @@ private fun formatUIDateToStored(inputDateString: String?, valueType: ValueType?
}
private fun isValid(valueString: String, valueType: ValueType?): Boolean {
- return when (valueType) {
- ValueType.DATE -> {
- valueString.length == 8
- }
+ return if (valueString.isEmpty()) {
+ true
+ } else {
+ when (valueType) {
+ ValueType.DATE -> {
+ valueString.length == 8
+ }
- ValueType.TIME -> {
- valueString.length == 4
- }
+ ValueType.TIME -> {
+ valueString.length == 4
+ }
- ValueType.DATETIME -> {
- valueString.length == 12
- }
+ ValueType.DATETIME -> {
+ valueString.length == 12
+ }
- else -> {
- valueString.length == 8
+ else -> {
+ valueString.length == 8
+ }
}
}
}
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt
index 6a05563409..d9fc95c0e1 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt
@@ -1,12 +1,8 @@
package org.dhis2.form.ui.provider.inputfield
-import android.content.Context
import android.content.Intent
-import android.text.TextWatcher
-import android.view.ContextThemeWrapper
-import android.view.LayoutInflater
-import android.view.ViewGroup
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
@@ -18,22 +14,19 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.onFocusEvent
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.viewinterop.AndroidViewBinding
-import androidx.databinding.DataBindingUtil
-import androidx.databinding.ViewDataBinding
import kotlinx.coroutines.launch
import org.dhis2.commons.resources.ResourceManager
-import org.dhis2.form.BR
-import org.dhis2.form.R
import org.dhis2.form.extensions.autocompleteList
import org.dhis2.form.extensions.inputState
import org.dhis2.form.extensions.legend
import org.dhis2.form.extensions.supportingText
import org.dhis2.form.model.FieldUiModel
import org.dhis2.form.model.UiRenderType
-import org.dhis2.form.ui.LatitudeLongitudeTextWatcher
import org.dhis2.form.ui.event.RecyclerViewUiEvents
import org.dhis2.form.ui.intent.FormIntent
import org.hisp.dhis.android.core.common.ValueType
@@ -43,6 +36,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputLetter
import org.hisp.dhis.mobile.ui.designsystem.component.InputLink
import org.hisp.dhis.mobile.ui.designsystem.component.InputLongText
import org.hisp.dhis.mobile.ui.designsystem.component.InputNegativeInteger
+import org.hisp.dhis.mobile.ui.designsystem.component.InputNotSupported
import org.hisp.dhis.mobile.ui.designsystem.component.InputNumber
import org.hisp.dhis.mobile.ui.designsystem.component.InputOrgUnit
import org.hisp.dhis.mobile.ui.designsystem.component.InputPercentage
@@ -55,21 +49,21 @@ import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations
@Composable
internal fun FieldProvider(
modifier: Modifier,
- context: Context,
fieldUiModel: FieldUiModel,
- needToForceUpdate: Boolean,
- textWatcher: TextWatcher,
- coordinateTextWatcher: LatitudeLongitudeTextWatcher,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
intentHandler: (FormIntent) -> Unit,
resources: ResourceManager,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
+ val context = LocalContext.current
val bringIntoViewRequester = remember { BringIntoViewRequester() }
+ val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope()
val modifierWithFocus = modifier
.bringIntoViewRequester(bringIntoViewRequester)
- .onFocusEvent {
+ .focusRequester(focusRequester)
+ .onFocusChanged {
if (it.isFocused && !fieldUiModel.focused) {
scope.launch {
bringIntoViewRequester.bringIntoView()
@@ -77,87 +71,116 @@ internal fun FieldProvider(
}
}
}
+ .focusable()
+
+ val modifierWithFocusForText = modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .onFocusChanged {
+ if (it.isFocused && !fieldUiModel.focused) {
+ scope.launch {
+ bringIntoViewRequester.bringIntoView()
+ fieldUiModel.onItemClick()
+ }
+ }
+ }
+
if (fieldUiModel.optionSet == null) {
when (fieldUiModel.valueType) {
ValueType.TEXT -> {
ProvideInputsForValueTypeText(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
)
}
ValueType.INTEGER_POSITIVE -> {
ProvideIntegerPositive(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
)
}
ValueType.INTEGER_ZERO_OR_POSITIVE -> {
ProvideIntegerPositiveOrZero(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.PERCENTAGE -> {
ProvidePercentage(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.NUMBER -> {
ProvideNumber(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.INTEGER_NEGATIVE -> {
ProvideIntegerNegative(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.LONG_TEXT -> {
ProvideLongText(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.LETTER -> {
ProvideLetter(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
ValueType.INTEGER -> {
ProvideInteger(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
@@ -167,24 +190,28 @@ internal fun FieldProvider(
fieldUiModel = fieldUiModel,
uiEventHandler = uiEventHandler,
intentHandler = intentHandler,
+ focusRequester = focusRequester,
)
}
ValueType.UNIT_INTERVAL -> {
ProvideUnitIntervalInput(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
+ onNextClicked = onNextClicked,
)
}
ValueType.EMAIL -> {
ProvideEmail(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
@@ -193,16 +220,19 @@ internal fun FieldProvider(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
resources = resources,
+ uiEventHandler = uiEventHandler,
)
}
ValueType.URL -> {
ProvideInputLink(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
@@ -216,6 +246,7 @@ internal fun FieldProvider(
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
resources = resources,
+ focusRequester = focusRequester,
)
}
@@ -225,6 +256,7 @@ internal fun FieldProvider(
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
resources = resources,
+ focusRequester = focusRequester,
)
}
}
@@ -245,6 +277,7 @@ internal fun FieldProvider(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
+ focusRequester = focusRequester,
)
}
}
@@ -252,11 +285,13 @@ internal fun FieldProvider(
ValueType.PHONE_NUMBER -> {
ProvideInputPhoneNumber(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
+
)
}
@@ -265,10 +300,11 @@ internal fun FieldProvider(
ValueType.TIME,
-> {
ProvideInputDate(
- modifier = modifierWithFocus,
+ modifier = modifierWithFocusForText,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
+ onNextClicked = onNextClicked,
)
}
@@ -278,6 +314,8 @@ internal fun FieldProvider(
ProvideInputSignature(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
+ intentHandler = intentHandler,
+ uiEventHandler = uiEventHandler,
)
}
@@ -285,6 +323,8 @@ internal fun FieldProvider(
ProvideInputImage(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
+ intentHandler = intentHandler,
+ uiEventHandler = uiEventHandler,
resources = resources,
)
}
@@ -306,6 +346,7 @@ internal fun FieldProvider(
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
resources = resources,
+ focusRequester = focusRequester,
)
}
}
@@ -320,26 +361,14 @@ internal fun FieldProvider(
resources = resources,
)
}
-
- else -> {
- AndroidViewBinding(
- modifier = modifier.fillMaxWidth(),
- factory = { inflater, viewgroup, add ->
- getFieldView(
- context,
- inflater,
- viewgroup,
- add,
- fieldUiModel.layoutId,
- needToForceUpdate,
- )
- },
- update = {
- this.setVariable(BR.textWatcher, textWatcher)
- this.setVariable(BR.coordinateWatcher, coordinateTextWatcher)
- this.setVariable(BR.item, fieldUiModel)
- },
- )
+ ValueType.REFERENCE,
+ ValueType.GEOJSON,
+ ValueType.USERNAME,
+ ValueType.TRACKER_ASSOCIATE,
+ ValueType.MULTI_TEXT,
+ null,
+ -> {
+ InputNotSupported(title = fieldUiModel.label)
}
}
} else {
@@ -351,6 +380,7 @@ internal fun FieldProvider(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
+ focusRequester = focusRequester,
)
}
@@ -361,6 +391,7 @@ internal fun FieldProvider(
modifier = modifierWithFocus,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
+ focusRequester = focusRequester,
)
}
@@ -400,8 +431,9 @@ private fun ProvideIntegerPositive(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -413,6 +445,7 @@ private fun ProvideIntegerPositive(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -436,8 +469,10 @@ private fun ProvideIntegerPositiveOrZero(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -449,6 +484,7 @@ private fun ProvideIntegerPositiveOrZero(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -472,8 +508,10 @@ private fun ProvidePercentage(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -485,6 +523,7 @@ private fun ProvidePercentage(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -508,8 +547,10 @@ private fun ProvideNumber(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -521,6 +562,7 @@ private fun ProvideNumber(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -545,8 +587,10 @@ private fun ProvideIntegerNegative(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value?.replace("-", ""))
}
@@ -558,6 +602,7 @@ private fun ProvideIntegerNegative(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -581,8 +626,10 @@ private fun ProvideLongText(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -594,6 +641,7 @@ private fun ProvideLongText(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -618,8 +666,10 @@ private fun ProvideLetter(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -631,6 +681,7 @@ private fun ProvideLetter(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -654,8 +705,10 @@ private fun ProvideInteger(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -667,6 +720,7 @@ private fun ProvideInteger(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -691,8 +745,9 @@ private fun ProvideEmail(
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -704,6 +759,7 @@ private fun ProvideEmail(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -737,8 +793,10 @@ private fun ProvideInputPhoneNumber(
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
focusManager: FocusManager,
modifier: Modifier = Modifier,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -750,6 +808,7 @@ private fun ProvideInputPhoneNumber(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -783,8 +842,10 @@ private fun ProvideInputLink(
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
+
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
@@ -796,6 +857,7 @@ private fun ProvideInputLink(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -828,6 +890,7 @@ private fun ProvideOrgUnitInput(
fieldUiModel: FieldUiModel,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
intentHandler: (FormIntent) -> Unit,
+ focusRequester: FocusRequester,
) {
var inputFieldValue by remember(
fieldUiModel,
@@ -845,7 +908,7 @@ private fun ProvideOrgUnitInput(
isRequiredField = fieldUiModel.mandatory,
onValueChanged = {
inputFieldValue = it
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
intentHandler(
FormIntent.OnSave(
fieldUiModel.uid,
@@ -855,7 +918,7 @@ private fun ProvideOrgUnitInput(
)
},
onOrgUnitActionCLicked = {
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
uiEventHandler.invoke(
RecyclerViewUiEvents.OpenOrgUnitDialog(
fieldUiModel.uid,
@@ -867,31 +930,3 @@ private fun ProvideOrgUnitInput(
)
}
-
-private fun getFieldView(
- context: Context,
- inflater: LayoutInflater,
- viewgroup: ViewGroup,
- add: Boolean,
- layoutId: Int,
- needToForceUpdate: Boolean,
-): ViewDataBinding {
- val layoutInflater =
- if (needToForceUpdate) {
- inflater.cloneInContext(
- ContextThemeWrapper(
- context,
- R.style.searchFormInputText,
- ),
- )
- } else {
- inflater.cloneInContext(
- ContextThemeWrapper(
- context,
- R.style.formInputText,
- ),
- )
- }
-
- return DataBindingUtil.inflate(layoutInflater, layoutId, viewgroup, add)
-}
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt
index 967bb9a78e..93bcab9de8 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt
@@ -17,6 +17,8 @@ import org.dhis2.form.extensions.supportingText
import org.dhis2.form.model.FieldUiModel
import org.dhis2.form.model.UiEventType
import org.dhis2.form.ui.binding.getBitmap
+import org.dhis2.form.ui.event.RecyclerViewUiEvents
+import org.dhis2.form.ui.intent.FormIntent
import org.hisp.dhis.mobile.ui.designsystem.component.InputImage
import org.hisp.dhis.mobile.ui.designsystem.component.UploadState
@@ -25,8 +27,10 @@ internal fun ProvideInputImage(
modifier: Modifier,
fieldUiModel: FieldUiModel,
resources: ResourceManager,
+ intentHandler: (FormIntent) -> Unit,
+ uiEventHandler: (RecyclerViewUiEvents) -> Unit,
) {
- var uploadState by remember(fieldUiModel) { mutableStateOf(getUploadState(fieldUiModel)) }
+ var uploadState by remember(fieldUiModel) { mutableStateOf(getUploadState(fieldUiModel.displayName, fieldUiModel.isLoadingData)) }
val painter = fieldUiModel.displayName?.getBitmap()?.let { BitmapPainter(it.asImageBitmap()) }
InputImage(
@@ -42,17 +46,32 @@ internal fun ProvideInputImage(
load = {
painter
},
- onDownloadButtonClick = { fieldUiModel.invokeUiEvent(UiEventType.SHOW_PICTURE) },
- onResetButtonClicked = { fieldUiModel.onClear() },
+ onDownloadButtonClick = {
+ uiEventHandler.invoke(RecyclerViewUiEvents.OpenFile(fieldUiModel))
+ },
+ onResetButtonClicked = {
+ fieldUiModel.onClear()
+ uploadState = getUploadState(fieldUiModel.displayName, false)
+ intentHandler.invoke(
+ FormIntent.OnAddImageFinished(
+ uid = fieldUiModel.uid,
+ ),
+ )
+ },
onAddButtonClicked = {
- uploadState = UploadState.UPLOADING
+ uploadState = getUploadState(fieldUiModel.displayName, true)
fieldUiModel.invokeUiEvent(UiEventType.ADD_PICTURE)
},
+ onImageClick = {
+ uiEventHandler.invoke(RecyclerViewUiEvents.ShowImage(fieldUiModel.label, fieldUiModel.displayName ?: ""))
+ },
)
}
-private fun getUploadState(fieldUiModel: FieldUiModel): UploadState {
- return if (fieldUiModel.displayName.isNullOrEmpty()) {
+internal fun getUploadState(value: String?, isLoading: Boolean): UploadState {
+ return if (isLoading && value.isNullOrEmpty()) {
+ UploadState.UPLOADING
+ } else if (value.isNullOrEmpty()) {
UploadState.ADD
} else {
UploadState.LOADED
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt
index ea551dc4f7..41d9f3e396 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.form.R
@@ -13,6 +14,7 @@ import org.dhis2.form.extensions.legend
import org.dhis2.form.extensions.supportingText
import org.dhis2.form.model.FieldUiModel
import org.dhis2.form.model.UiEventType
+import org.dhis2.form.ui.event.RecyclerViewUiEvents
import org.dhis2.ui.model.InputData
import org.hisp.dhis.mobile.ui.designsystem.component.InputFileResource
import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState
@@ -23,25 +25,20 @@ internal fun ProvideInputFileResource(
modifier: Modifier,
fieldUiModel: FieldUiModel,
resources: ResourceManager,
+ uiEventHandler: (RecyclerViewUiEvents) -> Unit,
) {
- val uploadState by remember(fieldUiModel) { mutableStateOf(getUploadState(fieldUiModel)) }
+ var uploadState by remember(fieldUiModel) { mutableStateOf(getFileUploadState(fieldUiModel.displayName, fieldUiModel.isLoadingData)) }
- val fileInputData = fieldUiModel.value?.let {
- val file = File(it)
- InputData.FileInputData(
- fileName = file.name,
- fileSize = file.length(),
- filePath = file.path,
- )
- }
-
- val filename by remember(fieldUiModel) {
- mutableStateOf(fileInputData?.fileName)
- }
+ val fileInputData =
+ fieldUiModel.displayName?.let {
+ val file = File(it)
+ InputData.FileInputData(
+ fileName = file.name,
+ fileSize = file.length(),
+ filePath = file.path,
+ )
+ }
- val fileSize by remember(fieldUiModel) {
- mutableStateOf(fileInputData?.fileSizeLabel)
- }
InputFileResource(
modifier = modifier.fillMaxWidth(),
title = fieldUiModel.label,
@@ -49,22 +46,26 @@ internal fun ProvideInputFileResource(
supportingText = fieldUiModel.supportingText(),
buttonText = resources.getString(R.string.add_file),
uploadFileState = uploadState,
- fileName = filename,
- fileWeight = fileSize,
+ fileName = fileInputData?.fileName,
+ fileWeight = fileInputData?.fileSizeLabel,
onSelectFile = {
+ uploadState = getFileUploadState(fieldUiModel.displayName, true)
fieldUiModel.invokeUiEvent(UiEventType.ADD_FILE)
},
onClear = { fieldUiModel.onClear() },
onUploadFile = {
- fieldUiModel.invokeUiEvent(UiEventType.OPEN_FILE)
+ uploadState = getFileUploadState(fieldUiModel.displayName, false)
+ uiEventHandler.invoke(RecyclerViewUiEvents.OpenFile(fieldUiModel))
},
legendData = fieldUiModel.legend(),
isRequired = fieldUiModel.mandatory,
)
}
-private fun getUploadState(fieldUiModel: FieldUiModel): UploadFileState {
- return if (fieldUiModel.displayName.isNullOrEmpty()) {
+private fun getFileUploadState(value: String?, isLoading: Boolean): UploadFileState {
+ return if (isLoading && value.isNullOrEmpty()) {
+ UploadFileState.UPLOADING
+ } else if (value.isNullOrEmpty()) {
UploadFileState.ADD
} else {
UploadFileState.LOADED
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt
index 3bfddbcd0d..2e2aec7626 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt
@@ -27,17 +27,20 @@ internal fun ProvideInputsForValueTypeText(
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
when (fieldUiModel.renderingType) {
- UiRenderType.QR_CODE -> {
+ UiRenderType.QR_CODE, UiRenderType.GS1_DATAMATRIX -> {
ProvideQRInput(
modifier = modifier,
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
)
}
+
UiRenderType.BAR_CODE -> {
ProvideBarcodeInput(
modifier = modifier,
@@ -45,6 +48,7 @@ internal fun ProvideInputsForValueTypeText(
intentHandler = intentHandler,
uiEventHandler = uiEventHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
)
} else -> {
ProvideDefaultTextInput(
@@ -52,6 +56,7 @@ internal fun ProvideInputsForValueTypeText(
fieldUiModel = fieldUiModel,
intentHandler = intentHandler,
focusManager = focusManager,
+ onNextClicked = onNextClicked,
)
}
}
@@ -64,6 +69,7 @@ private fun ProvideQRInput(
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
@@ -77,6 +83,7 @@ private fun ProvideQRInput(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -121,6 +128,7 @@ private fun ProvideDefaultTextInput(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
var value by remember {
mutableStateOf(fieldUiModel.value)
@@ -133,6 +141,7 @@ private fun ProvideDefaultTextInput(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
@@ -157,6 +166,7 @@ private fun ProvideBarcodeInput(
intentHandler: (FormIntent) -> Unit,
uiEventHandler: (RecyclerViewUiEvents) -> Unit,
focusManager: FocusManager,
+ onNextClicked: () -> Unit,
) {
var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
@@ -170,6 +180,7 @@ private fun ProvideBarcodeInput(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt
index 5242d97213..9406a26405 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MatrixInputProvider.kt
@@ -38,7 +38,7 @@ internal fun ProvideMatrixInput(
icon = "dhis2_$icon"
}
val iconCardItem = IconCardData(
- uid = fieldUiModel.uid,
+ uid = option.code() ?: "",
label = option.displayName() ?: "",
iconRes = icon,
iconTint = Color(color),
@@ -48,7 +48,7 @@ internal fun ProvideMatrixInput(
iconCardItem,
)
}
- if (fieldUiModel.displayName == option.displayName()) matrixSelectedItem = iconCardItem
+ if (fieldUiModel.displayName == option.code() || fieldUiModel.displayName == option.displayName()) matrixSelectedItem = iconCardItem
}
InputMatrix(
@@ -63,7 +63,7 @@ internal fun ProvideMatrixInput(
newSelectedItem
}
fieldUiModel.onItemClick()
- val valueToSave = if (matrixSelectedItem == null) null else matrixSelectedItem?.label
+ val valueToSave = if (matrixSelectedItem == null) null else matrixSelectedItem?.uid
intentHandler(
FormIntent.OnSave(
fieldUiModel.uid,
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt
index bd7c271104..1bc114e682 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/RadioButtonProvider.kt
@@ -2,6 +2,7 @@ package org.dhis2.form.ui.provider.inputfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
import org.dhis2.commons.resources.ResourceManager
import org.dhis2.form.R
import org.dhis2.form.extensions.inputState
@@ -18,6 +19,7 @@ internal fun ProvideRadioButtonInput(
modifier: Modifier,
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
+ focusRequester: FocusRequester,
) {
val data = fieldUiModel.optionSetConfiguration?.optionsToDisplay()?.map { option ->
RadioButtonData(
@@ -39,7 +41,7 @@ internal fun ProvideRadioButtonInput(
isRequired = fieldUiModel.mandatory,
itemSelected = data.find { it.selected },
onItemChange = { item ->
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
intentHandler(
FormIntent.OnSave(
fieldUiModel.uid,
@@ -58,6 +60,7 @@ internal fun ProvideYesNoRadioButtonInput(
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
resources: ResourceManager,
+ focusRequester: FocusRequester,
) {
val data = listOf(
RadioButtonData(
@@ -85,7 +88,7 @@ internal fun ProvideYesNoRadioButtonInput(
isRequired = fieldUiModel.mandatory,
itemSelected = data.find { it.selected },
onItemChange = { item ->
- fieldUiModel.onItemClick()
+ focusRequester.requestFocus()
when (item?.uid) {
"true" -> {
intentHandler(
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt
index 153ac5e878..2d51a480d0 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SequentialInputProvider.kt
@@ -33,14 +33,12 @@ internal fun ProvideSequentialInput(
fieldUiModel.optionSetConfiguration?.optionsToDisplay()?.forEach() { option ->
val color =
ObjectStyleUtils.getColorResource(context, option.style().color(), R.color.colorPrimary)
- option.style().icon()?.let {
- }
var icon = option.style().icon() ?: "dhis2_default_outline"
if (!icon.startsWith("dhis2_")) {
icon = "dhis2_$icon"
}
val iconCardItem = IconCardData(
- uid = fieldUiModel.uid,
+ uid = option.code() ?: "",
label = option.displayName() ?: "",
iconRes = icon,
iconTint = Color(color),
@@ -50,7 +48,7 @@ internal fun ProvideSequentialInput(
iconCardItem,
)
}
- if (fieldUiModel.displayName == option.displayName()) matrixSelectedItem = iconCardItem
+ if (fieldUiModel.displayName == option.code() || fieldUiModel.displayName == option.displayName()) matrixSelectedItem = iconCardItem
}
InputSequential(
@@ -65,7 +63,7 @@ internal fun ProvideSequentialInput(
newSelectedItem
}
fieldUiModel.onItemClick()
- val valueToSave = if (matrixSelectedItem == null) null else matrixSelectedItem?.label
+ val valueToSave = if (matrixSelectedItem == null) null else matrixSelectedItem?.uid
intentHandler(
FormIntent.OnSave(
fieldUiModel.uid,
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt
index 979c158509..0bc7dccae6 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt
@@ -2,6 +2,10 @@ package org.dhis2.form.ui.provider.inputfield
import android.graphics.BitmapFactory
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
@@ -11,14 +15,17 @@ import org.dhis2.form.extensions.legend
import org.dhis2.form.extensions.supportingText
import org.dhis2.form.model.FieldUiModel
import org.dhis2.form.model.UiEventType
+import org.dhis2.form.ui.event.RecyclerViewUiEvents
+import org.dhis2.form.ui.intent.FormIntent
import org.hisp.dhis.mobile.ui.designsystem.component.InputSignature
-import org.hisp.dhis.mobile.ui.designsystem.component.UploadState
import java.io.File
@Composable
fun ProvideInputSignature(
modifier: Modifier,
fieldUiModel: FieldUiModel,
+ intentHandler: (FormIntent) -> Unit,
+ uiEventHandler: (RecyclerViewUiEvents) -> Unit,
) {
val imageBitmap: ImageBitmap? = fieldUiModel.displayName?.let { path ->
File(path)
@@ -27,6 +34,8 @@ fun ProvideInputSignature(
?.asImageBitmap()
}
+ var uploadState by remember(fieldUiModel) { mutableStateOf(getUploadState(fieldUiModel.displayName, fieldUiModel.isLoadingData)) }
+
InputSignature(
modifier = modifier,
title = fieldUiModel.label,
@@ -34,7 +43,7 @@ fun ProvideInputSignature(
supportingText = fieldUiModel.supportingText(),
legendData = fieldUiModel.legend(),
isRequired = fieldUiModel.mandatory,
- uploadState = if (fieldUiModel.value != null) UploadState.LOADED else UploadState.ADD,
+ uploadState = uploadState,
load = { imageBitmap },
painterFor = imageBitmap?.let {
{
@@ -42,8 +51,24 @@ fun ProvideInputSignature(
BitmapPainter(image!!)
}
},
- onDownloadButtonClick = { fieldUiModel.invokeUiEvent(UiEventType.SHOW_PICTURE) },
- onResetButtonClicked = { fieldUiModel.onClear() },
- onAddButtonClicked = { fieldUiModel.invokeUiEvent(UiEventType.ADD_SIGNATURE) },
+ onDownloadButtonClick = {
+ uiEventHandler.invoke(RecyclerViewUiEvents.OpenFile(fieldUiModel))
+ },
+ onResetButtonClicked = {
+ fieldUiModel.onClear()
+ uploadState = getUploadState(fieldUiModel.displayName, false)
+ intentHandler.invoke(
+ FormIntent.OnAddImageFinished(
+ uid = fieldUiModel.uid,
+ ),
+ )
+ },
+ onAddButtonClicked = {
+ uploadState = getUploadState(fieldUiModel.displayName, true)
+ fieldUiModel.invokeUiEvent(UiEventType.ADD_SIGNATURE)
+ },
+ onImageClick = {
+ uiEventHandler.invoke(RecyclerViewUiEvents.ShowImage(fieldUiModel.label, fieldUiModel.displayName ?: ""))
+ },
)
}
diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt
index a482ed659c..9c4558ae90 100644
--- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/UnitIntervalInputProvider.kt
@@ -19,8 +19,9 @@ fun ProvideUnitIntervalInput(
modifier: Modifier,
fieldUiModel: FieldUiModel,
intentHandler: (FormIntent) -> Unit,
+ onNextClicked: () -> Unit,
) {
- var value by remember {
+ var value by remember(fieldUiModel.value) {
mutableStateOf(fieldUiModel.value)
}
InputUnitInterval(
@@ -31,6 +32,7 @@ fun ProvideUnitIntervalInput(
legendData = fieldUiModel.legend(),
inputText = value ?: "",
isRequiredField = fieldUiModel.mandatory,
+ onNextClicked = onNextClicked,
onValueChanged = {
value = it
intentHandler(
diff --git a/form/src/main/java/org/dhis2/form/ui/validation/FieldErrorMessageProvider.kt b/form/src/main/java/org/dhis2/form/ui/validation/FieldErrorMessageProvider.kt
index 2d7c344651..e646bcecec 100644
--- a/form/src/main/java/org/dhis2/form/ui/validation/FieldErrorMessageProvider.kt
+++ b/form/src/main/java/org/dhis2/form/ui/validation/FieldErrorMessageProvider.kt
@@ -169,7 +169,7 @@ class FieldErrorMessageProvider(private val context: Context) {
}
fun mandatoryWarning(): String {
- return context.getString(R.string.field_is_mandatory)
+ return context.getString(R.string.required)
}
fun defaultValidationErrorMessage(): String {
diff --git a/form/src/main/res/values-zh/strings.xml b/form/src/main/res/values-zh/strings.xml
index 691384b705..67b33d6384 100644
--- a/form/src/main/res/values-zh/strings.xml
+++ b/form/src/main/res/values-zh/strings.xml
@@ -98,4 +98,8 @@
没有更多保留值,请联系管理员报名日期事件日期
-
\ No newline at end of file
+ 扫描
+ 二维码
+ 条码
+ 添加位置
+
\ No newline at end of file
diff --git a/form/src/main/res/values/strings.xml b/form/src/main/res/values/strings.xml
index 0d1c82f113..f02c6f7d84 100644
--- a/form/src/main/res/values/strings.xml
+++ b/form/src/main/res/values/strings.xml
@@ -23,6 +23,7 @@
Program rule %s is trying to assign value %s which does not belongs to option set %sValue for field %s does not belong to the option set in %sThis field is mandatory
+ RequiredDateYearsMonths
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f11be467b2..a69103b153 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ ndk = "21.4.7075529"
sdk = "34"
minSdk = "21"
vCode = "128"
-vName = "2.9-DEV"
+vName = "2.9"
kotlinCompilerExtensionVersion = "1.5.2"
@@ -13,8 +13,8 @@ hilt = '2.47'
hiltCompiler = '1.0.0'
jacoco = '0.8.10'
-designSystem = "1.0-20231025.123540-96"
-dhis2sdk = "1.9.0-20231025.143704-59"
+designSystem = "1.0-20231116.084101-124"
+dhis2sdk = "1.9.0"
ruleEngine = "2.1.9"
appcompat = "1.6.1"
annotation = "1.6.0"
diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt
index ff58831cf2..fb82c1158b 100644
--- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt
+++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt
@@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
import com.jakewharton.rxrelay2.PublishRelay
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.disposables.CompositeDisposable
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -324,7 +325,10 @@ class ManageStockViewModel @Inject constructor(
}
fun onSaveValueChange(cell: TableCell) {
- viewModelScope.launch(dispatcherProvider.io()) {
+ viewModelScope.launch(
+ dispatcherProvider.io(),
+ start = CoroutineStart.ATOMIC,
+ ) {
saveValue(cell)
}
}
diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US
index 86166afc29..c4aaf6a80f 100644
--- a/whatsnew/whatsnew-en-US
+++ b/whatsnew/whatsnew-en-US
@@ -1,9 +1,12 @@
-This is a Patch version that fixes:
-ANDROAPP-3106 [Bug]Error when searching with comma char in the values
-ANDROAPP-4322 Analytics legends don't show event's exact date
-ANDROAPP-4710 Mandatory Fields
-ANDROAPP-5253 Event status filter doesn't remove checkmarks after the reset
-ANDROAPP-5255 Loading bar never hides in overview screen
-ANDROAPP-5271 After finishing transaction, orgunit on the Dialog is not reset
+This new version includes the following features:
+- New design for Dataset, Event and TEI cards
+- Implement changes in TEI Dashboard details
+- Data entry forms redesigned
+- Disable collapsible sections in forms
+- Disable referral in tracker programs
+- [EXPERIMENTAL] TEI Header
+- Move working lists under the search bar
+- Skip home if users only have access to one program
+- Display program stage description
You can find all details in Github (github.com/dhis2/dhis2-android-capture-app) releases
\ No newline at end of file