diff --git a/.github/workflows/release-generate-notes.yml b/.github/workflows/release-generate-notes.yml index 81c112255b..f944abc7ab 100644 --- a/.github/workflows/release-generate-notes.yml +++ b/.github/workflows/release-generate-notes.yml @@ -22,19 +22,16 @@ jobs: steps: - uses: actions/checkout@v3 + with: + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} + # Generate github release notes - name: Generate release notes working-directory: ./scripts run: python3 generateReleaseNotes.py - - name: setup git config - run: | - # setup the username and email. - git config user.name "GitHub Actions Bot" - git config user.email "" - - name: Commit changes - run: | - # Commit and push - git commit -am "Update release notes" - git push + uses: flex-development/gh-commit@1.0.0 + with: + message: "Update release notes" + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index 538e250e51..0bfafd685b 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -2,11 +2,8 @@ name: Release start -# Controls when the action will run. Workflow runs when manually triggered using the UI -# or API. on: workflow_dispatch: - # Inputs the workflow accepts. inputs: release_version_name: description: 'New release version name' @@ -18,72 +15,87 @@ on: required: true type: string -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: create_branch: - # The type of runner that the job will run on runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ inputs.release_version_name }} + RELEASE_BRANCH: 'release/${{ inputs.release_version_name }}' + TEMP_RELEASE_BRANCH: 'tmp_release/${{ inputs.release_version_name }}' steps: - name: Check out code uses: actions/checkout@v4 + with: + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.12.1 - - name: setup git config + # Creates an auxiliary branch. This branch will be used to create the signed commit using the GH API. + # It is required to use an auxiliary branch because the RELEASE_BRANCH is protected and the GH API + # rejects the commit even though the user identified by the token is included in the bypass list. + - name: Create auxiliary branch run: | - # setup the username and email. - git config user.name "GitHub Actions Bot" - git config user.email "" - - # override vName with new version - - name: Create release branch - run: git checkout -b release/${{ inputs.release_version_name }} + git checkout -b ${{ env.TEMP_RELEASE_BRANCH }} + git push origin ${{ env.TEMP_RELEASE_BRANCH }} - name: Run Python script to update release branch version - run: python scripts/updateVersionName.py ${{ inputs.release_version_name }} + run: python scripts/updateVersionName.py ${{ env.RELEASE_VERSION }} + + # Uses the GH API to create the signed commit. + - name: Commit and Push Changes to auxiliary branch + uses: flex-development/gh-commit@1.0.0 + with: + message: 'Update version to ${{ env.RELEASE_VERSION }}' + ref: ${{ env.TEMP_RELEASE_BRANCH }} + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} - - name: Push + # Fetch the remote commit (signed commit) and create a new branch with the RELEASE_BRANCH name. + # This is required because the RELEASE_BRANCH is protected. + - name: Create and push release branch run: | - git add . - git commit -m "Update version to ${{ inputs.release_version_name }}" - git push origin release/${{ inputs.release_version_name }} + git reset --hard + git pull origin ${{ env.TEMP_RELEASE_BRANCH }} + git checkout -b ${{ env.RELEASE_BRANCH }} + git push origin ${{ env.RELEASE_BRANCH }} + git push origin --delete ${{ env.TEMP_RELEASE_BRANCH }} update_version: - # The type of runner that the job will run on runs-on: ubuntu-latest + env: + DEVELOPMENT_VERSION: ${{ inputs.development_version_name }} + DEVELOPMENT_BRANCH: 'update_version_to${{ inputs.development_version_name }}' steps: - name: Check out code uses: actions/checkout@v4 + with: + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.12.1 - - name: setup git config + - name: Create development branch run: | - # setup the username and email. - git config user.name "GitHub Actions Bot" - git config user.email "" - - - name: Create release branch - run: git checkout -b update_version_to${{ inputs.development_version_name }} + git checkout -b ${{ env.DEVELOPMENT_BRANCH }} + git push origin ${{ env.DEVELOPMENT_BRANCH }} - name: Run Python script to update base branch version - run: python scripts/updateVersionName.py ${{ inputs.development_version_name }} + run: python scripts/updateVersionName.py ${{ env.DEVELOPMENT_VERSION }} - name: Commit and Push Changes - run: | - git add . - git commit -m "Update version to ${{ inputs.development_version_name }}" - git push origin update_version_to${{ inputs.development_version_name }} + uses: flex-development/gh-commit@1.0.0 + with: + message: 'Update version to ${{ env.DEVELOPMENT_VERSION }}' + ref: ${{ env.DEVELOPMENT_BRANCH }} + token: ${{ secrets.DHIS2_BOT_GITHUB_TOKEN }} - - name: create pull request - run: gh pr create -B develop -H update_version_to${{ inputs.development_version_name }} --title 'Merge update_version_to${{ inputs.development_version_name }} into develop' --body 'Created by Github action' + - name: Create pull request + run: gh pr create -B develop -H update_version_to${{ env.DEVELOPMENT_VERSION }} --title 'Merge ${{ env.DEVELOPMENT_BRANCH }} into develop' --body 'Created by Github action' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Jenkinsfile b/Jenkinsfile index 1cafcbd059..969b719aff 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,10 +1,13 @@ +//Sets cron schedule just for PUSH job +String cron_string = JOB_NAME.startsWith('android-multibranch-PUSH') ? '0 0 * * *' : '' + pipeline { agent { label "ec2-android" } triggers { - cron('0 0 * * *') + cron(cron_string) } options { diff --git a/RELEASE.md b/RELEASE.md index 39a76ba947..72f1b71d8f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,9 +1,89 @@ -# Release notes - Android App for DHIS2 - 3.1.0.1 +# Release notes - Android App for DHIS2 - 3.1.1 ### Bug -[ANDROAPP-6653](https://dhis2.atlassian.net/browse/ANDROAPP-6653) Large option sets freeze the app +[ANDROAPP-5888](https://dhis2.atlassian.net/browse/ANDROAPP-5888) RTSM - Stock distribution allows entry of zero values -[ANDROAPP-6665](https://dhis2.atlassian.net/browse/ANDROAPP-6665) Filters persists when exiting the program or data set +[ANDROAPP-6108](https://dhis2.atlassian.net/browse/ANDROAPP-6108) \[mobile-ui\] Bottom sheet Icon button is displaced -[ANDROAPP-6691](https://dhis2.atlassian.net/browse/ANDROAPP-6691) NullPointerException: Dataset table \ No newline at end of file +[ANDROAPP-6220](https://dhis2.atlassian.net/browse/ANDROAPP-6220) Login error - Fragmentation has been destroyed + +[ANDROAPP-6281](https://dhis2.atlassian.net/browse/ANDROAPP-6281) Formatting of org unit selector buttons over android navigation bar + +[ANDROAPP-6282](https://dhis2.atlassian.net/browse/ANDROAPP-6282) Misalignment of TEI list and dashboard cards + +[ANDROAPP-6317](https://dhis2.atlassian.net/browse/ANDROAPP-6317) App not scrolling to the top of the list after selecting or deselecting working list + +[ANDROAPP-6354](https://dhis2.atlassian.net/browse/ANDROAPP-6354) \[LineListing\] search by org unit or category no working + +[ANDROAPP-6394](https://dhis2.atlassian.net/browse/ANDROAPP-6394) Conflict message - Change future date message when no future date was entered + +[ANDROAPP-6418](https://dhis2.atlassian.net/browse/ANDROAPP-6418) \[Data Sets\] Values are not shown after saving them + +[ANDROAPP-6440](https://dhis2.atlassian.net/browse/ANDROAPP-6440) Maps view pin hidden behind location + +[ANDROAPP-6457](https://dhis2.atlassian.net/browse/ANDROAPP-6457) Maps card - expanded card maybe more than 70% of the map and When the user taps outside the card \(when expanded\) the card does not collapses. + +[ANDROAPP-6477](https://dhis2.atlassian.net/browse/ANDROAPP-6477) Programs are hidden after navigating to settings and back + +[ANDROAPP-6480](https://dhis2.atlassian.net/browse/ANDROAPP-6480) Tei scheduled events for "today" show incorrect overdue icon + +[ANDROAPP-6482](https://dhis2.atlassian.net/browse/ANDROAPP-6482) TEI Is created even if it was discarded + +[ANDROAPP-6509](https://dhis2.atlassian.net/browse/ANDROAPP-6509) Assign enrollment org unit as the default org unit when creating an event + +[ANDROAPP-6519](https://dhis2.atlassian.net/browse/ANDROAPP-6519) Barcode scanner crashes DHIS2-RTS + +[ANDROAPP-6524](https://dhis2.atlassian.net/browse/ANDROAPP-6524) Option code is displayed instead of name + +[ANDROAPP-6533](https://dhis2.atlassian.net/browse/ANDROAPP-6533) Background color is visible behind bottom navigation bar + +[ANDROAPP-6535](https://dhis2.atlassian.net/browse/ANDROAPP-6535) Bidirectional relationships are created in the oppsite direction when created from TO + +[ANDROAPP-6543](https://dhis2.atlassian.net/browse/ANDROAPP-6543) Image download not working correctly + +[ANDROAPP-6549](https://dhis2.atlassian.net/browse/ANDROAPP-6549) DHIS2-RTS: Data entry window not closing when selecting blue bullet + +[ANDROAPP-6579](https://dhis2.atlassian.net/browse/ANDROAPP-6579) Limit the menu size in the enrollment dashboard when the program name is too long. + +[ANDROAPP-6628](https://dhis2.atlassian.net/browse/ANDROAPP-6628) Due Date in Scheduled Event Card Always Red + +[ANDROAPP-6644](https://dhis2.atlassian.net/browse/ANDROAPP-6644) Incorrect style and icon for overdue scheduled events in + +[ANDROAPP-6652](https://dhis2.atlassian.net/browse/ANDROAPP-6652) NullPointerException: Attempt to invoke virtual method 'android.content.res.Resources android.view.View.getResources\(\)'... + +[ANDROAPP-6657](https://dhis2.atlassian.net/browse/ANDROAPP-6657) Implement a load bar when searching in "This Area" + +[ANDROAPP-6660](https://dhis2.atlassian.net/browse/ANDROAPP-6660) Incorrect list of available periods + +[ANDROAPP-6667](https://dhis2.atlassian.net/browse/ANDROAPP-6667) \[Program Rules\] Warning AND Error on complete not showing + +[ANDROAPP-6673](https://dhis2.atlassian.net/browse/ANDROAPP-6673) Home filters are not displayed + +[ANDROAPP-6695](https://dhis2.atlassian.net/browse/ANDROAPP-6695) IllegalArgumentException: com.dhis2: Targeting S\+ \(version 31 and above\) requires that one of FLAG\_IMMUTABLE or FLAG\_MUTABL... + +[ANDROAPP-6696](https://dhis2.atlassian.net/browse/ANDROAPP-6696) IllegalStateException: Can not perform this action after onSaveInstanceState + +[ANDROAPP-6705](https://dhis2.atlassian.net/browse/ANDROAPP-6705) Tei attributes and data elements flagging allowed future dates as errors incorrectly in Form + +[ANDROAPP-6707](https://dhis2.atlassian.net/browse/ANDROAPP-6707) Unable to complete event + +[ANDROAPP-6715](https://dhis2.atlassian.net/browse/ANDROAPP-6715) Crash when selecting a checkbox option set + +[ANDROAPP-6717](https://dhis2.atlassian.net/browse/ANDROAPP-6717) Crash when syncing a TEI from map screen + +[ANDROAPP-6753](https://dhis2.atlassian.net/browse/ANDROAPP-6753) Event program takes too long to load in server https://data.zim-dreams.org + +[ANDROAPP-6759](https://dhis2.atlassian.net/browse/ANDROAPP-6759) Version comparison returns a wrong value in some cases + +[ANDROAPP-6760](https://dhis2.atlassian.net/browse/ANDROAPP-6760) Rule engine context events include deleted events + +[ANDROAPP-6763](https://dhis2.atlassian.net/browse/ANDROAPP-6763) Status bar overlaps the app, obstructing buttons and making the interface unreadable + +[ANDROAPP-6771](https://dhis2.atlassian.net/browse/ANDROAPP-6771) Crash when creating a tei after searching through org unit + +[ANDROAPP-6774](https://dhis2.atlassian.net/browse/ANDROAPP-6774) Bottomsheet buttons padding fix + +[ANDROAPP-6776](https://dhis2.atlassian.net/browse/ANDROAPP-6776) Incorrect background shown in EventInitial screen after Android 35 corrections + +[ANDROAPP-6777](https://dhis2.atlassian.net/browse/ANDROAPP-6777) inconsistent behaviour in org unit selector search bar \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c808767b48..de5b427240 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -283,15 +283,6 @@ dependencies { coreLibraryDesugaring(libs.desugar) - debugImplementation(libs.analytics.flipper) - debugImplementation(libs.analytics.soloader) - debugImplementation(libs.analytics.flipper.network) - debugImplementation(libs.analytics.flipper.leak) - debugImplementation(libs.analytics.leakcanary) - - releaseImplementation(libs.analytics.leakcanary.noop) - releaseImplementation(libs.analytics.flipper.noop) - "dhisPlayServicesImplementation"(libs.google.auth) "dhisPlayServicesImplementation"(libs.google.auth.apiphone) diff --git a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt index 322263b0b5..d49d8ce14d 100644 --- a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt @@ -1,16 +1,11 @@ package org.dhis2.common.filters import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.TypeTextAction 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 import androidx.test.espresso.matcher.ViewMatchers.withId import org.dhis2.R import org.dhis2.common.BaseRobot -import org.dhis2.common.matchers.DatePickerMatchers.Companion.matchesDate -import org.dhis2.commons.filters.FilterHolder fun filterRobotCommon(robotBody: FiltersRobot.() -> Unit) { FiltersRobot().apply { @@ -19,43 +14,11 @@ fun filterRobotCommon(robotBody: FiltersRobot.() -> Unit) { } class FiltersRobot : BaseRobot() { - fun openFilterAtPosition(position: Int) { - onView(withId(R.id.filterRecyclerLayout)).perform( - RecyclerViewActions.actionOnItemAtPosition(position, click()) - ) - } - - fun clickOnFromToDateOption() { - onView(withId(R.id.fromTo)).perform(click()) - } - - fun clickOnOrgUnitTree() { - onView(withId(R.id.ouTreeButton)).perform(click()) - } fun selectDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { onView(withId(R.id.datePicker)).perform( PickerActions.setDate(year, monthOfYear, dayOfMonth) ) - } - - fun typeOrgUnit(orgUnitName: String) { - onView(withId(R.id.orgUnitSearchEditText)).perform(TypeTextAction(orgUnitName)) - } - - fun clickAddOrgUnit() { - onView(withId(R.id.addButton)).perform(click()) - } - - fun selectNotSyncedState() { - onView(withId(R.id.stateNotSynced)).perform(click()) - } - - fun acceptDateSelected() { onView(withId(R.id.acceptBtn)).perform(click()) } - - fun checkDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { - onView(withId(R.id.datePicker)).check(matches(matchesDate(year, monthOfYear, dayOfMonth))) - } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/keystore/KeyStoreRobot.kt b/app/src/androidTest/java/org/dhis2/common/keystore/KeyStoreRobot.kt index 2b2c1abc2d..36b99d8dbc 100644 --- a/app/src/androidTest/java/org/dhis2/common/keystore/KeyStoreRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/keystore/KeyStoreRobot.kt @@ -17,7 +17,7 @@ class KeyStoreRobot(private val keystore: AndroidSecureStore) { } companion object { - const val KEYSTORE_USERNAME = "username" + const val KEYSTORE_USERNAME = "android" const val KEYSTORE_PASSWORD = "password" const val USERNAME = "android" const val PASSWORD = "Android123" diff --git a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt index cd07f23e3d..641cfed1b4 100644 --- a/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/mockwebserver/MockWebServerRobot.kt @@ -17,11 +17,14 @@ class MockWebServerRobot(private val dhis2MockServer: Dhis2MockServer) { } companion object { - const val API_OLD_TRACKED_ENTITY_PATH = "/api/trackedEntityInstances/query?.*" - const val API_OLD_TRACKED_ENTITY_RESPONSE = - "mocks/teilist/old_tracked_entity_empty_response.json" - const val API_OLD_EVENTS_PATH = "/api/events?.*" - const val API_OLD_EVENTS_RESPONSE = "mocks/teilist/old_events_empty_response.json" - + const val API_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_PATH = + "/api/trackedEntityAttributes/lZGmxYbs97q/generateAndReserve?.*" + const val API_TRACKED_ENTITY_ATTRIBUTES_RESERVED_VALUES_RESPONSE = + "mocks/teidashboard/tracked_entity_attribute_reserved_values.json" + const val API_TRACKED_ENTITY_PATH = "/api/tracker/trackedEntities?.*" + const val API_TRACKED_ENTITY_EMPTY_RESPONSE = + "mocks/teilist/tracked_entity_empty_response.json" + const val API_EVENTS_PATH = "/api/tracker/events?.*" + const val API_EVENTS_EMPTY_RESPONSE = "mocks/teilist/events_empty_response.json" } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt index b08151e87e..fab2d84887 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt @@ -6,6 +6,8 @@ import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import dhis2.org.analytics.charts.idling.AnalyticsCountingIdlingResource +import java.util.concurrent.TimeUnit import org.dhis2.AppTest import org.dhis2.AppTest.Companion.DB_TO_IMPORT import org.dhis2.common.BaseRobot @@ -22,16 +24,15 @@ import org.dhis2.commons.idlingresource.CountingIdlingResourceSingleton import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.prefs.Preference import org.dhis2.form.ui.idling.FormCountingIdlingResource +import org.dhis2.maps.utils.OnMapReadyIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.programEventDetail.eventList.EventListIdlingResourceSingleton import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton -import org.dhis2.maps.utils.OnMapReadyIdlingResourceSingleton import org.junit.After import org.junit.Before import org.junit.ClassRule import org.junit.Rule import org.junit.rules.Timeout -import java.util.concurrent.TimeUnit open class BaseTest { @@ -86,6 +87,7 @@ open class BaseTest { TeiDataIdlingResourceSingleton.countingIdlingResource, EventIdlingResourceSingleton.countingIdlingResource, OnMapReadyIdlingResourceSingleton.countingIdlingResource, + AnalyticsCountingIdlingResource.countingIdlingResource, ) } @@ -98,6 +100,7 @@ open class BaseTest { SearchIdlingResourceSingleton.countingIdlingResource, TeiDataIdlingResourceSingleton.countingIdlingResource, EventIdlingResourceSingleton.countingIdlingResource, + AnalyticsCountingIdlingResource.countingIdlingResource, ) } 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 baea240a96..432552229c 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt @@ -46,123 +46,80 @@ class DataSetTest : BaseTest() { } } - @Ignore("Indeterministic it will be addressed in ANDROAPP-6458") @Test fun shouldCreateNewDataSet() { - val period = "Aug 2024" + val period = "Jul 2025" val orgUnit = "Ngelehun CHC" - startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) + startDataSetDetailActivity( + "BfMAe6Itzgt", + "Child Health", + ruleDataSetDetail + ) dataSetDetailRobot { clickOnAddDataSet() } dataSetInitialRobot { clickOnInputOrgUnit() - orgUnitSelectorRobot(composeTestRule) { - selectTreeOrgUnit(orgUnit) - } + } + + orgUnitSelectorRobot(composeTestRule) { + selectTreeOrgUnit(orgUnit) + } + + dataSetInitialRobot { clickOnInputPeriod() selectPeriod(period) clickOnActionButton() } dataSetTableRobot(composeTestRule) { - typeOnCell("bjDvmb4bfuf", 0, 0) + typeOnCell("dzjKKQq0cSO", 0, 0) clickOnEditValue() typeInput("1") - composeTestRule.waitForIdle() pressBack() - composeTestRule.waitForIdle() pressBack() - composeTestRule.waitForIdle() clickOnSaveButton() - waitToDebounce(500) + clickOnNegativeButton() clickOnNegativeButton() } - } - - @Test - fun shouldOpenAndEditDataset() { - startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) dataSetRobot { clickOnDataSetAtPosition(0) } dataSetTableRobot(composeTestRule) { - typeOnCell("bjDvmb4bfuf", 0, 0) + typeOnCell("dzjKKQq0cSO", 0, 1) clickOnEditValue() typeInput("5") - composeTestRule.waitForIdle() pressBack() - composeTestRule.waitForIdle() pressBack() - composeTestRule.waitForIdle() clickOnSaveButton() - waitToDebounce(500) clickOnNegativeButton() - } - } - - @Test - fun shouldReopenModifyAndCompleteDataset() { - startDataSetDetailActivity("V8MHeZHIrcP", "Facility Assessment", ruleDataSetDetail) - - dataSetRobot { - clickOnDataSetAtPosition(0) - } - - dataSetTableRobot(composeTestRule) { - openMenuMoreOptions() - clickOnMenuReOpen() clickOnPositiveButton() - typeOnCell("bjDvmb4bfuf", 0, 0) - clickOnAcceptDate() - clickOnSaveButton() - waitToDebounce(500) - clickOnPositiveButton() - } - dataSetDetailRobot { - checkDataSetIsCompleteAndModified("2019") } - } @Test - fun shouldBlockSelectingNewCellIfCurrentHasError() { - startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) + fun shouldSelectNewCellIfCurrentHasNoErrorAndBlockSelectingNewCellIfCurrentHasError() { + startDataSetDetailActivity("BfMAe6Itzgt", "Child Health", ruleDataSetDetail) dataSetRobot { clickOnDataSetAtPosition(0) } dataSetTableRobot(composeTestRule) { - typeOnCell("bjDvmb4bfuf", 0, 0) + typeOnCell("dzjKKQq0cSO", 0, 0) clickOnEditValue() - typeInput("5,,") + typeInput("5") composeTestRule.waitForIdle() composeTestRule.onNodeWithTag(INPUT_TEST_FIELD_TEST_TAG).performImeAction() - composeTestRule.waitForIdle() - assertCellSelected("bjDvmb4bfuf", 0, 0) - } - } - - @Test - fun shouldSelectNewCellIfCurrentHasNoError() { - startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) + assertCellSelected("dzjKKQq0cSO", 0, 1) - dataSetRobot { - clickOnDataSetAtPosition(0) - } - - dataSetTableRobot(composeTestRule) { - typeOnCell("bjDvmb4bfuf", 0, 0) clickOnEditValue() - typeInput("5") + typeInput("5,,") composeTestRule.waitForIdle() composeTestRule.onNodeWithTag(INPUT_TEST_FIELD_TEST_TAG).performImeAction() - composeTestRule.waitForIdle() - waitToDebounce(500) - assertCellSelected("bjDvmb4bfuf", 1, 0) + assertCellSelected("dzjKKQq0cSO", 0, 1) } } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt index 0b8ab3b41e..2e2a9040f0 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventIntents.kt @@ -16,8 +16,8 @@ const val ENROLLMENT_UID = "ENROLLMENT_UID" const val PROGRAM_STAGE_UID = "PROGRAM_STAGE_UID" const val PROGRAM_TB_UID = "ur1Edk5Oe2n" -const val PROGRAM_XX_TRACKER_UID = "U5KybNCtA3E" -const val EVENT_DETAILS_UID = "oPCuUeDGaIu" +const val ANTENATAL_CARE_PROGRAM_UID = "lxAQ7Zs9VYR" +const val ANTENATAL_CARE_EVENT_UID = "ohAH6BXIMad" const val EVENT_TO_SHARE_UID = "y0xoVIzBpnL" const val TEI_EVENT_TO_DELETE_UID = "foc5zag6gbE" const val ENROLLMENT_EVENT_DELETE_UID = "SolDyMgW3oc" @@ -30,8 +30,8 @@ fun prepareEventDetailsIntentAndLaunchActivity(rule: LazyActivityScenarioRule - - \ No newline at end of file diff --git a/app/src/debug/java/org.dhis2/data/appinspector/AppInspector.kt b/app/src/debug/java/org.dhis2/data/appinspector/AppInspector.kt deleted file mode 100644 index b9c2f707c8..0000000000 --- a/app/src/debug/java/org.dhis2/data/appinspector/AppInspector.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.dhis2.data.appinspector - -import android.content.Context -import android.os.Build -import com.facebook.flipper.android.AndroidFlipperClient -import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin -import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin -import com.facebook.flipper.plugins.inspector.DescriptorMapping -import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin -import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor -import com.facebook.flipper.plugins.network.NetworkFlipperPlugin -import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin -import com.facebook.soloader.SoLoader -import org.dhis2.BuildConfig - -class AppInspector(private val context: Context) { - var flipperInterceptor: FlipperOkhttpInterceptor? = null - private set - - fun init(): AppInspector { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { - SoLoader.init(context, false) - if (BuildConfig.DEBUG && BuildConfig.FLAVOR != "dhisUITesting") { - AndroidFlipperClient.getInstance(context).apply { - addPlugin( - layoutInspectorPlugin(), - ) - addPlugin( - databaseInspectorPlugin(), - ) - addPlugin( - networkInspectorPlugin(), - ) - addPlugin( - sharedPreferencesPlugin(), - ) - addPlugin( - crashPlugin(), - ) - start() - } - } - } - return this - } - - private fun layoutInspectorPlugin() = - InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()) - - private fun databaseInspectorPlugin() = DatabasesFlipperPlugin(context) - private fun networkInspectorPlugin() = NetworkFlipperPlugin().also { - flipperInterceptor = FlipperOkhttpInterceptor(it) - } - - private fun sharedPreferencesPlugin() = SharedPreferencesFlipperPlugin(context) - - private fun crashPlugin() = CrashReporterPlugin.getInstance() -} diff --git a/app/src/dhisUITesting/assets/databases/dhis_test.db b/app/src/dhisUITesting/assets/databases/dhis_test.db index a14b48ebcc..38860bd6dc 100644 Binary files a/app/src/dhisUITesting/assets/databases/dhis_test.db and b/app/src/dhisUITesting/assets/databases/dhis_test.db differ diff --git a/app/src/dhisUITesting/assets/mocks/teidashboard/tracked_entity_attribute_reserved_values.json b/app/src/dhisUITesting/assets/mocks/teidashboard/tracked_entity_attribute_reserved_values.json new file mode 100644 index 0000000000..d797725e7d --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teidashboard/tracked_entity_attribute_reserved_values.json @@ -0,0 +1,10 @@ +[ + { + "ownerObject": "TRACKEDENTITYATTRIBUTE", + "ownerUid": "lZGmxYbs97q", + "key": "RANDOM(###)", + "value": "046", + "created": "2018-04-26T14:54:53.344", + "expiryDate": "2100-06-25T14:54:53.344" + } +] \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/events_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/events_empty_response.json new file mode 100644 index 0000000000..7b9d80087e --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/events_empty_response.json @@ -0,0 +1,9 @@ +{ + "pager": { + "page": 1, + "pageSize": 30 + }, + "page": 1, + "pageSize": 30, + "events": [] +} \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json deleted file mode 100644 index c03b0880c9..0000000000 --- a/app/src/dhisUITesting/assets/mocks/teilist/old_events_empty_response.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "pager": { - "page": 1, - "pageCount": 1, - "total": 2, - "pageSize": 50 - }, - "events": [] -} \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json deleted file mode 100644 index e5104f914d..0000000000 --- a/app/src/dhisUITesting/assets/mocks/teilist/old_tracked_entity_empty_response.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "headers": [ - { - "name": "instance", - "column": "Instance", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "created", - "column": "Created", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "lastupdated", - "column": "Last updated", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "ou", - "column": "Organisation unit", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "ouname", - "column": "Organisation unit name", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "te", - "column": "Tracked entity type", - "type": "java.lang.String", - "hidden": false, - "meta": false - }, - { - "name": "inactive", - "column": "Inactive", - "type": "java.lang.String", - "hidden": false, - "meta": false - } - ], - "metaData": { - "names": { - "nEenWmSyUEp": "Person" - } - }, - "width": 9, - "height": 0, - "rows": [ - ] -} \ No newline at end of file diff --git a/app/src/dhisUITesting/assets/mocks/teilist/tracked_entity_empty_response.json b/app/src/dhisUITesting/assets/mocks/teilist/tracked_entity_empty_response.json new file mode 100644 index 0000000000..c14396ace0 --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/teilist/tracked_entity_empty_response.json @@ -0,0 +1,9 @@ +{ + "pager": { + "page": 1, + "pageSize": 30 + }, + "page": 1, + "pageSize": 30, + "trackedEntities": [] +} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/App.java b/app/src/main/java/org/dhis2/App.java index afe22a35e1..c32fdb3434 100644 --- a/app/src/main/java/org/dhis2/App.java +++ b/app/src/main/java/org/dhis2/App.java @@ -33,7 +33,6 @@ import org.dhis2.commons.schedulers.SchedulersProviderImpl; import org.dhis2.commons.service.SessionManagerModule; import org.dhis2.commons.sync.SyncComponentProvider; -import org.dhis2.data.appinspector.AppInspector; import org.dhis2.data.dispatcher.DispatcherModule; import org.dhis2.data.server.SSLContextInitializer; import org.dhis2.data.server.ServerComponent; @@ -103,7 +102,6 @@ public class App extends MultiDexApplication implements Components, LifecycleObs private boolean fromBackGround = false; private boolean recreated; - private AppInspector appInspector; @Override public void onCreate() { @@ -111,8 +109,6 @@ public void onCreate() { ProcessLifecycleOwner.get().getLifecycle().addObserver(this); - appInspector = new AppInspector(this).init(); - MapController.Companion.init(this); setUpAppComponent(); @@ -360,10 +356,6 @@ private void setUpRxPlugin() { }); } - public AppInspector getAppInspector() { - return appInspector; - } - @Override public FeatureConfigActivityComponent provideFeatureConfigActivityComponent() { return userComponent.plus(new FeatureConfigActivityModule()); diff --git a/app/src/main/java/org/dhis2/bindings/Extensions.kt b/app/src/main/java/org/dhis2/bindings/Extensions.kt index b9febcd56c..ae4ea9c1a9 100644 --- a/app/src/main/java/org/dhis2/bindings/Extensions.kt +++ b/app/src/main/java/org/dhis2/bindings/Extensions.kt @@ -20,6 +20,7 @@ import java.text.DecimalFormat fun MutableLiveData.default(initialValue: T) = this.apply { setValue(initialValue) } +@Deprecated("Use ProfilePictureProvider instead") fun TrackedEntityInstance.profilePicturePath(d2: D2, programUid: String?): String { var path: String? = null diff --git a/app/src/main/java/org/dhis2/bindings/StringExtensions.kt b/app/src/main/java/org/dhis2/bindings/StringExtensions.kt index 7c48014b4e..480775d03b 100644 --- a/app/src/main/java/org/dhis2/bindings/StringExtensions.kt +++ b/app/src/main/java/org/dhis2/bindings/StringExtensions.kt @@ -6,6 +6,7 @@ import org.dhis2.commons.date.toDateSpan import timber.log.Timber import java.util.Date +const val WRONG_FORMAT = "Wrong format" val String?.initials: String get() { val userNames = this @@ -34,27 +35,27 @@ fun String.toDate(): Date { try { date = DateUtils.databaseDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(WRONG_FORMAT) } if (date == null) { try { date = DateUtils.databaseDateFormatNoZulu().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(WRONG_FORMAT) } } if (date == null) { try { date = DateUtils.dateTimeFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(WRONG_FORMAT) } } if (date == null) { try { date = DateUtils.uiDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(WRONG_FORMAT) } } @@ -62,12 +63,12 @@ fun String.toDate(): Date { try { date = DateUtils.oldUiDateFormat().parse(this) } catch (e: Exception) { - Timber.d("wrong format") + Timber.d(WRONG_FORMAT) } } if (date == null) { - throw NullPointerException("$this can't be parse to Date") + throw NullPointerException("$this can't be null or empty") } return date @@ -83,7 +84,7 @@ fun String.newVersion(oldVersion: String): Boolean { val old = oldVersion.split(".") try { new.forEachIndexed { index, vNumber -> - if (vNumber.toInt() < (old.getOrElse(index) { "0" }).toInt()) return false + if (vNumber.toInt() < (old.getOrElse(index) { "0" }).toInt()) return false else if (vNumber.toInt() > (old.getOrElse(index) { "0" }).toInt()) return true } } catch (e: Exception) { return false diff --git a/app/src/main/java/org/dhis2/data/server/ServerModule.kt b/app/src/main/java/org/dhis2/data/server/ServerModule.kt index 75d2462eb2..99a36b7ef4 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerModule.kt +++ b/app/src/main/java/org/dhis2/data/server/ServerModule.kt @@ -12,6 +12,8 @@ import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.commons.di.dagger.PerServer import org.dhis2.commons.filters.data.GetFiltersApplyingWebAppConfig +import org.dhis2.commons.periods.data.EventPeriodRepository +import org.dhis2.commons.periods.domain.GetEventPeriods import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ColorUtils @@ -172,14 +174,21 @@ class ServerModule { return ResourceManager(contextWrapper, colorUtils) } + @Provides + @PerServer + fun provideEventPeriodRepository(d2: D2): EventPeriodRepository = + EventPeriodRepository(d2) + + @Provides + @PerServer + fun providePeriodUseCase(eventPeriodRepository: EventPeriodRepository) = + GetEventPeriods(eventPeriodRepository) + companion object { @JvmStatic fun getD2Configuration(context: Context): D2Configuration { val interceptors: MutableList = ArrayList() - context.app().appInspector.flipperInterceptor?.let { flipperInterceptor -> - interceptors.add(flipperInterceptor) - } interceptors.add( AnalyticsInterceptor( AnalyticsHelper(context.app().appComponent().matomoController()), 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 b0b066eb22..2757c5bea8 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java +++ b/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java @@ -21,10 +21,10 @@ import org.dhis2.App; import org.dhis2.R; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.prefs.PreferenceProvider; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.Constants; -import org.dhis2.utils.DateUtils; import org.dhis2.utils.NetworkUtils; import org.hisp.dhis.android.core.maintenance.D2Error; diff --git a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt index d5205fb81c..1581671b75 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt @@ -9,6 +9,7 @@ import io.reactivex.Observable import org.dhis2.bindings.toSeconds import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.bindings.program +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.prefs.Preference.Companion.DATA import org.dhis2.commons.prefs.Preference.Companion.EVENT_MAX import org.dhis2.commons.prefs.Preference.Companion.EVENT_MAX_DEFAULT @@ -24,7 +25,6 @@ import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.data.service.workManager.WorkManagerController import org.dhis2.data.service.workManager.WorkerItem import org.dhis2.data.service.workManager.WorkerType -import org.dhis2.utils.DateUtils import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.matomo.DEFAULT_EXTERNAL_TRACKER_NAME import org.hisp.dhis.android.core.D2 diff --git a/app/src/main/java/org/dhis2/model/SnackbarMessage.kt b/app/src/main/java/org/dhis2/model/SnackbarMessage.kt new file mode 100644 index 0000000000..26f907b7f2 --- /dev/null +++ b/app/src/main/java/org/dhis2/model/SnackbarMessage.kt @@ -0,0 +1,8 @@ +package org.dhis2.model + +import java.util.UUID + +data class SnackbarMessage( + val id: UUID = UUID.randomUUID(), + val message: String = "", +) diff --git a/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt b/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt index 00b07f1b59..93b26678a9 100644 --- a/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt @@ -40,12 +40,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cat.ereza.customactivityoncrash.CustomActivityOnCrash import cat.ereza.customactivityoncrash.config.CaocConfig -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.BuildConfig import org.dhis2.R import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -61,7 +61,7 @@ class CrashActivity : AppCompatActivity() { return } setContent { - MdcTheme { + DHIS2Theme { Scaffold( floatingActionButton = { CrashGoBackButton { diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableRepositoryImpl.kt index 64fd93cc27..0c4b2e0bca 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableRepositoryImpl.kt @@ -320,7 +320,7 @@ class DataSetTableRepositoryImpl( .`in`(UidsHelper.getUidsList(categoryOptionCombos)) dataValueRepository.blockingGet().isNotEmpty() && dataValueRepository - .blockingGet().size != categoryOptionCombos.size + .blockingCount() != categoryOptionCombos.size }?.map { dataSetElement -> dataSetElement.dataElement().uid() } ?: emptyList() } else { diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt index b2c7b0f4f3..69a1db0003 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt @@ -8,7 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.DatePicker -import androidx.compose.material.MaterialTheme +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -19,7 +19,6 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp -import com.google.android.material.composethemeadapter.MdcTheme import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import org.dhis2.R @@ -27,6 +26,7 @@ import org.dhis2.bindings.toDate import org.dhis2.commons.Constants.ACCESS_DATA import org.dhis2.commons.Constants.DATA_SET_SECTION import org.dhis2.commons.Constants.DATA_SET_UID +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.dialogs.DialogClickListener import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener @@ -46,7 +46,6 @@ import org.dhis2.data.forms.dataentry.tablefields.spinner.SpinnerViewModel import org.dhis2.usescases.datasets.dataSetTable.DataSetTableActivity import org.dhis2.usescases.datasets.dataSetTable.DataSetTablePresenter import org.dhis2.usescases.general.FragmentGlobalAbstract -import org.dhis2.utils.DateUtils import org.dhis2.utils.customviews.OptionSetOnClickListener import org.dhis2.utils.customviews.TableFieldDialog import org.dhis2.utils.optionset.OptionSetDialog @@ -55,6 +54,7 @@ import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.ValueTypeRenderingType import org.hisp.dhis.android.core.dataelement.DataElement import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -106,7 +106,7 @@ class DataSetSectionFragment : FragmentGlobalAbstract(), DataValueContract.View return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - MdcTheme { + DHIS2Theme { val localDensity = LocalDensity.current val conf = LocalConfiguration.current val tableConfState by presenterFragment.currentTableConfState().collectAsState() @@ -191,9 +191,9 @@ class DataSetSectionFragment : FragmentGlobalAbstract(), DataValueContract.View TableTheme( tableColors = TableColors( - primary = MaterialTheme.colors.primary, - primaryLight = MaterialTheme.colors.primary.copy(alpha = 0.2f), - disabledSelectedBackground = MaterialTheme.colors.primary.copy( + primary = MaterialTheme.colorScheme.primary, + primaryLight = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + disabledSelectedBackground = MaterialTheme.colorScheme.primary.copy( alpha = 0.5f, ), ), diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt index 4da96acd03..36cee56b76 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValueRepository.kt @@ -618,63 +618,65 @@ class DataValueRepository( options, ) - val valueStateSyncState = d2.dataValueModule().dataValues() - .byDataSetUid(dataSetUid) - .byPeriod().eq(periodId) - .byOrganisationUnitUid().eq(orgUnitUid) - .byAttributeOptionComboUid().eq(attributeOptionComboUid) - .byDataElementUid().eq(dataElement.uid()) - .byCategoryOptionComboUid().eq(categoryOptionCombo.uid()) - .blockingGet() - .find { it.dataElement() == dataElement.uid() } - ?.syncState() - - val conflictInField = - conflicts.takeIf { - when (valueStateSyncState) { - State.ERROR, - State.WARNING, - -> true - - else -> false - } - }?.filter { - "${it.dataElement()}_${it.categoryOptionCombo()}" == fieldViewModel.uid() - }?.takeIf { it.isNotEmpty() }?.map { it.displayDescription() ?: "" } - - val error = errors[fieldViewModel.uid()] - - val errorList = when { - valueStateSyncState == State.ERROR && - conflictInField != null && - error != null -> - conflictInField + listOf(error) + if (conflicts.isNotEmpty()) { + val valueStateSyncState = d2.dataValueModule().dataValues() + .byDataSetUid(dataSetUid) + .byPeriod().eq(periodId) + .byOrganisationUnitUid().eq(orgUnitUid) + .byAttributeOptionComboUid().eq(attributeOptionComboUid) + .byDataElementUid().eq(dataElement.uid()) + .byCategoryOptionComboUid().eq(categoryOptionCombo.uid()) + .blockingGet() + .find { it.dataElement() == dataElement.uid() } + ?.syncState() + + val conflictInField = + conflicts.takeIf { + when (valueStateSyncState) { + State.ERROR, + State.WARNING, + -> true + + else -> false + } + }?.filter { + "${it.dataElement()}_${it.categoryOptionCombo()}" == fieldViewModel.uid() + }?.takeIf { it.isNotEmpty() }?.map { it.displayDescription() ?: "" } - valueStateSyncState == State.ERROR && conflictInField != null -> - conflictInField + val error = errors[fieldViewModel.uid()] - error != null -> - listOf(error) + val errorList = when { + valueStateSyncState == State.ERROR && + conflictInField != null && + error != null -> + conflictInField + listOf(error) - else -> null - } + valueStateSyncState == State.ERROR && conflictInField != null -> + conflictInField - val warningList = when { - valueStateSyncState == State.WARNING && - conflictInField != null -> - conflictInField + error != null -> + listOf(error) - else -> - null - } + else -> null + } - fieldViewModel = errorList?.let { - fieldViewModel.withError(it.joinToString(".\n")) - } ?: fieldViewModel + val warningList = when { + valueStateSyncState == State.WARNING && + conflictInField != null -> + conflictInField - fieldViewModel = warningList?.let { - fieldViewModel.withWarning(warningList.joinToString(".\n")) - } ?: fieldViewModel + else -> + null + } + + fieldViewModel = errorList?.let { + fieldViewModel.withError(it.joinToString(".\n")) + } ?: fieldViewModel + + fieldViewModel = warningList?.let { + fieldViewModel.withWarning(warningList.joinToString(".\n")) + } ?: fieldViewModel + } fields.add(fieldViewModel) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java index 2daf359f0a..fb4b2cb8b9 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java @@ -23,6 +23,7 @@ import org.dhis2.bindings.ExtensionsKt; import org.dhis2.bindings.ViewExtensionsKt; import org.dhis2.commons.Constants; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.filters.FilterItem; import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.FiltersAdapter; @@ -32,7 +33,6 @@ import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.datasets.datasetDetail.datasetList.DataSetListFragment; import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.utils.DateUtils; import org.dhis2.utils.category.CategoryDialog; import org.dhis2.utils.granularsync.SyncStatusDialog; import org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt; @@ -197,7 +197,8 @@ public void showPeriodRequest(FilterManager.PeriodRequest periodRequest) { DateUtils.getInstance().showPeriodDialog( this, datePeriods -> filterManager.addPeriod(datePeriods), - true + true, + () -> filterManager.addPeriod(null) ); } } 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 82119871d1..97be6f1fad 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 @@ -59,7 +59,7 @@ public Flowable> dataSetGroups(List orgUnits, L d2.dataSetModule().dataSets().uid(dataSetUid).blockingGet(); int dataSetOrgUnitNumber = d2.organisationUnitModule().organisationUnits() .byDataSetUids(Collections.singletonList(dataSetUid)) - .blockingGet().size(); + .blockingCount(); DataSetInstanceCollectionRepository finalRepo = repo; return Flowable.fromIterable(finalRepo.blockingGet()) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailViewModelFactory.kt index 12984cc1dc..65b255fb9c 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailViewModelFactory.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.viewmodel.DispatcherProvider -@Suppress("UNCHECKED_CAST") class DataSetDetailViewModelFactory( private val dispatcherProvider: DispatcherProvider, private val dataSetPageConfigurator: DataSetPageConfigurator, diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt index cd7e07ef33..2e51f97d0e 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/DataSetListViewModelFactory.kt @@ -8,7 +8,6 @@ import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailRepository -@Suppress("UNCHECKED_CAST") class DataSetListViewModelFactory( val dataSetDetailRepository: DataSetDetailRepository, val schedulerProvider: SchedulerProvider, diff --git a/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt b/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt index a2da03a56f..8824555d90 100644 --- a/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt @@ -14,6 +14,7 @@ import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.bindings.fromCache import org.dhis2.commons.bindings.tei import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager @@ -41,6 +42,7 @@ class EventInfoProvider( private val dateLabelProvider: DateLabelProvider, private val metadataIconProvider: MetadataIconProvider, private val profilePictureProvider: ProfilePictureProvider, + private val dateUtils: DateUtils, ) { private val cachedPrograms = mutableMapOf() private val cachedDisplayOrgUnit = mutableMapOf() @@ -276,18 +278,19 @@ class EventInfoProvider( EventStatus.SCHEDULE -> { val text = dueDate.toOverdueOrScheduledUiText(resourceManager) - + val color = if (dateUtils.isEventDueDateOverdue(dueDate)) AdditionalInfoItemColor.ERROR.color else AdditionalInfoItemColor.SUCCESS.color + val iconVector = if (dateUtils.isEventDueDateOverdue(dueDate)) Icons.Outlined.EventBusy else Icons.Outlined.Event AdditionalInfoItem( icon = { Icon( - imageVector = Icons.Outlined.Event, + imageVector = iconVector, contentDescription = text, - tint = AdditionalInfoItemColor.SUCCESS.color, + tint = color, ) }, value = text, isConstantItem = true, - color = AdditionalInfoItemColor.SUCCESS.color, + color = color, ) } @@ -432,6 +435,6 @@ class EventInfoProvider( fromCache(cachedDisplayOrgUnit, programUid) { d2.organisationUnitModule().organisationUnits() .byProgramUids(listOf(programUid)) - .blockingGet().size > 1 + .blockingCount() > 1 } == true } 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 28952bf12a..7153b7c3c8 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt @@ -8,12 +8,17 @@ import androidx.compose.foundation.layout.Column import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.databinding.DataBindingUtil +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.App import org.dhis2.R import org.dhis2.commons.date.DateUtils -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.date.toUiStringResource +import org.dhis2.commons.dialogs.AlertBottomDialog +import org.dhis2.commons.periods.ui.PeriodSelectorContent import org.dhis2.databinding.ActivityEventScheduledBinding import org.dhis2.form.model.EventMode +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel @@ -145,7 +150,12 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. uiModel = EventInputDateUiModel( eventDate = eventDate, detailsEnabled = true, - onDateClick = { showEventDatePeriodDialog(programStage.periodType()) }, + onDateClick = { + showPeriodDialog( + periodType = programStage.periodType(), + scheduling = false, + ) + }, onDateSelected = {}, onClear = { }, required = true, @@ -160,7 +170,12 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. uiModel = EventInputDateUiModel( eventDate = dueDate, detailsEnabled = true, - onDateClick = { showDueDatePeriodDialog(programStage.periodType()) }, + onDateClick = { + showPeriodDialog( + periodType = programStage.periodType(), + scheduling = true, + ) + }, onDateSelected = {}, onClear = { }, required = true, @@ -181,58 +196,28 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. binding.name = program.displayName() } - private fun showEventDatePeriodDialog(periodType: PeriodType?) { - if (periodType != null) { - 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) - } - - PeriodDialog() - .setPeriod(periodType) - .setMinDate(minDate) - .setMaxDate(DateUtils.getInstance().today) - .setPossitiveListener { selectedDate -> presenter.setEventDate(selectedDate) } - .show(supportFragmentManager, PeriodDialog::class.java.simpleName) - } - } - - private fun showDueDatePeriodDialog(periodType: PeriodType?) { - if (periodType != null) { - 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) - } - - PeriodDialog() - .setPeriod(periodType) - .setMinDate(minDate) - .setMaxDate(DateUtils.getInstance().today) - .setPossitiveListener { selectedDate -> presenter.setDueDate(selectedDate) } - .show(supportFragmentManager, PeriodDialog::class.java.simpleName) - } + private fun showPeriodDialog(periodType: PeriodType?, scheduling: Boolean) { + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = getString((periodType ?: PeriodType.Daily).toUiStringResource()), + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { _ -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = presenter.fetchPeriods(scheduling).collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedPeriod -> + presenter.setDueDate(selectedPeriod) + bottomSheetDialog.dismiss() + } + }, + ).show(supportFragmentManager, AlertBottomDialog::class.java.simpleName) } override fun openFormActivity() { 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 dfc435ff34..103f7af9c9 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt @@ -1,5 +1,8 @@ package org.dhis2.usescases.events +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.model.Period import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues import org.dhis2.usescases.general.AbstractActivityContracts import org.hisp.dhis.android.core.category.CategoryOption @@ -32,5 +35,6 @@ class ScheduledEventContract { fun getEventTei(): String fun getEnrollment(): Enrollment? fun getSelectableDates(program: Program, isDueDate: Boolean): SelectableDates? + fun fetchPeriods(scheduling: Boolean): Flow> } } 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 fc8cebc39c..bbcff0660d 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt @@ -1,9 +1,17 @@ package org.dhis2.usescases.events +import androidx.paging.PagingData import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.dhis2.commons.bindings.event +import org.dhis2.commons.bindings.programStage import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.periods.data.EventPeriodRepository +import org.dhis2.commons.periods.domain.GetEventPeriods +import org.dhis2.commons.periods.model.Period import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MAX_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MIN_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues @@ -12,6 +20,7 @@ import org.hisp.dhis.android.core.arch.helpers.UidsHelper import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import timber.log.Timber @@ -27,6 +36,7 @@ class ScheduledEventPresenterImpl( ) : ScheduledEventContract.Presenter { private lateinit var disposable: CompositeDisposable + private val getEventPeriods = GetEventPeriods(EventPeriodRepository(d2)) override fun init() { disposable = CompositeDisposable() @@ -106,11 +116,37 @@ class ScheduledEventPresenterImpl( } else { null } - val minDateString = if (minDate == null) null else SimpleDateFormat("ddMMyyyy", Locale.US).format(minDate) - val maxDateString = if (isDueDate) DEFAULT_MAX_DATE else SimpleDateFormat("ddMMyyyy", Locale.US).format(Date(System.currentTimeMillis() - 1000)) + val minDateString = + if (minDate == null) null else SimpleDateFormat("ddMMyyyy", Locale.US).format(minDate) + val maxDateString = if (isDueDate) { + DEFAULT_MAX_DATE + } else { + SimpleDateFormat( + "ddMMyyyy", + Locale.US, + ).format(Date(System.currentTimeMillis() - 1000)) + } return SelectableDates(minDateString ?: DEFAULT_MIN_DATE, maxDateString) } + override fun fetchPeriods(scheduling: Boolean): Flow> { + val event = d2.event(eventUid) ?: return emptyFlow() + val stage = event.programStage()?.let { d2.programStage(it) } ?: return emptyFlow() + + return getEventPeriods( + eventUid = eventUid, + periodType = stage.periodType() ?: PeriodType.Daily, + selectedDate = if (scheduling) { + event.dueDate() + } else { + event.eventDate() + }, + programStage = stage, + isScheduling = scheduling, + eventEnrollmentUid = event.enrollment(), + ) + } + override fun setDueDate(date: Date) { d2.eventModule().events().uid(eventUid).setDueDate(date) d2.eventModule().events().uid(eventUid).setStatus(EventStatus.SCHEDULE) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt index 51c13c95d7..9ffb196d42 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt @@ -41,7 +41,7 @@ import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityEventCaptureBinding import org.dhis2.form.model.EventMode -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState import org.dhis2.ui.ThemeManager import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel 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 a70ef1c12c..652fd0759f 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 @@ -159,6 +159,8 @@ class EventDetailsRepository( else -> OrganisationUnit.Scope.SCOPE_DATA_CAPTURE } + fun isScheduling(): Boolean = eventCreationType == EventCreationType.SCHEDULE + fun getOrganisationUnit(orgUnitUid: String): OrganisationUnit? { return d2.organisationUnitModule().organisationUnits() .byUid() diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt new file mode 100644 index 0000000000..d724ed8b86 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt @@ -0,0 +1,33 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.dhis2.commons.periods.domain.GetEventPeriods +import org.dhis2.commons.periods.model.Period +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository +import org.hisp.dhis.android.core.period.PeriodType + +class ConfigurePeriodSelector( + private val enrollmentUid: String?, + private val eventDetailRepository: EventDetailsRepository, + private val getEventPeriods: GetEventPeriods, +) { + operator fun invoke(): Flow> { + val programStage = eventDetailRepository.getProgramStage() ?: return emptyFlow() + val event = eventDetailRepository.getEvent() + val periodType = programStage.periodType() ?: PeriodType.Daily + return getEventPeriods( + eventUid = event?.uid(), + periodType = periodType, + selectedDate = if (eventDetailRepository.isScheduling()) { + event?.dueDate() + } else { + event?.eventDate() + }, + programStage = programStage, + isScheduling = eventDetailRepository.isScheduling(), + eventEnrollmentUid = enrollmentUid, + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt index bb12ddfcd2..b8c19555b1 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt @@ -6,6 +6,7 @@ import dagger.Provides import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.periods.domain.GetEventPeriods import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.prefs.PreferenceProviderImpl import org.dhis2.commons.resources.DhisPeriodUtils @@ -30,6 +31,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsViewModelFactory @@ -101,6 +103,19 @@ class EventDetailsModule( ) } + @Provides + @PerFragment + fun provideConfigurePeriodSelector( + eventDetailsRepository: EventDetailsRepository, + periodUseCase: GetEventPeriods, + ): ConfigurePeriodSelector { + return ConfigurePeriodSelector( + enrollmentUid = enrollmentId, + eventDetailRepository = eventDetailsRepository, + getEventPeriods = periodUseCase, + ) + } + @Provides @PerFragment fun eventDetailsViewModelFactory( @@ -112,6 +127,7 @@ class EventDetailsModule( locationProvider: LocationProvider, eventDetailResourcesProvider: EventDetailResourcesProvider, metadataIconProvider: MetadataIconProvider, + configurePeriodSelector: ConfigurePeriodSelector, ): EventDetailsViewModelFactory { return EventDetailsViewModelFactory( ConfigureEventDetails( @@ -152,6 +168,7 @@ class EventDetailsModule( resourcesProvider = resourcesProvider, ), eventDetailResourcesProvider = eventDetailResourcesProvider, + configurePeriodSelector = configurePeriodSelector, ) } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDate.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDate.kt index ff99840afd..e63fd347cb 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDate.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventDate.kt @@ -13,4 +13,7 @@ data class EventDate( val scheduleInterval: Int = 0, val allowFutureDates: Boolean = true, val periodType: PeriodType? = null, -) + val error: Boolean = false, +) { + val isValid = !dateValue.isNullOrEmpty() && !error +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt index 601766d697..da79b3f911 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt @@ -14,4 +14,5 @@ data class EventInputDateUiModel( val showField: Boolean = true, val is24HourFormat: Boolean = true, val selectableDates: SelectableDates? = null, + val onError: (() -> Unit)? = null, ) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index 51e2978dc9..7c11a324e8 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import kotlinx.datetime.LocalDate import org.dhis2.R import org.dhis2.commons.extensions.inDateRange import org.dhis2.commons.extensions.inOrgUnit @@ -35,15 +36,14 @@ import org.hisp.dhis.mobile.ui.designsystem.component.DropdownInputField import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem 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.InputDateTimeModel 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.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import org.hisp.dhis.mobile.ui.designsystem.component.model.DateTransformation -import java.time.LocalDate -import java.time.format.DateTimeFormatter +import org.hisp.dhis.mobile.ui.designsystem.component.state.InputDateTimeData +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberInputDateTimeState import java.time.format.DateTimeParseException @Composable @@ -79,25 +79,13 @@ fun ProvideInputDate( } else { IntRange(1924, 2124) } - InputDateTime( - InputDateTimeModel( + val inputState = rememberInputDateTimeState( + InputDateTimeData( title = uiModel.eventDate.label ?: "", allowsManualInput = uiModel.allowsManualInput, - inputTextFieldValue = value, actionType = DateTimeActionType.DATE, - state = state, visualTransformation = DateTransformation(), - onValueChanged = { - value = it ?: TextFieldValue() - state = getInputShellStateBasedOnValue(it?.text) - it?.let { it1 -> manageActionBasedOnValue(uiModel, it1.text) } - }, isRequired = uiModel.required, - onFocusChanged = { focused -> - if (!focused && !isValid(value.text)) { - state = InputShellState.ERROR - } - }, is24hourFormat = uiModel.is24HourFormat, selectableDates = uiModel.selectableDates ?: SelectableDates( "01011924", @@ -105,23 +93,33 @@ fun ProvideInputDate( ), yearRange = yearRange, ), + inputTextFieldValue = value, + inputState = state, + ) + InputDateTime( + state = inputState, modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), + onValueChanged = { + value = it ?: TextFieldValue() + it?.let { dateValue -> + manageActionBasedOnValue( + uiModel = uiModel, + dateString = dateValue.text, + ) + } + }, + onFocusChanged = { focused -> + if (!focused && !isValid(value.text) && state == InputShellState.FOCUSED) { + state = InputShellState.ERROR + } + }, ) } } 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)) { + when (ValueType.DATE.validator.validate(dateString)) { is Result.Failure -> false is Result.Success -> true } @@ -130,27 +128,33 @@ fun isValidDateFormat(dateString: String): Boolean { } } -fun getInputShellStateBasedOnValue(dateString: String?): InputShellState { - dateString?.let { - return if (isValid(it) && !isValidDateFormat(it)) { - InputShellState.ERROR - } else { - InputShellState.FOCUSED - } - } - return InputShellState.FOCUSED -} - fun manageActionBasedOnValue(uiModel: EventInputDateUiModel, dateString: String) { if (dateString.isEmpty()) { uiModel.onClear?.invoke() - } else if (isValid(dateString) && isValidDateFormat(dateString)) { + } else if (isValidDateFormat(dateString)) { formatUIDateToStored(dateString)?.let { dateValues -> - uiModel.onDateSelected(dateValues) + if (uiModel.selectableDates?.let { dateValues.isInRange(it) } == true) { + uiModel.onDateSelected(dateValues) + } else { + uiModel.onError?.invoke() + } } + } else { + uiModel.onError?.invoke() } } +fun InputDateValues.isInRange(selectableDates: SelectableDates): Boolean { + val format = LocalDate.Format { + dayOfMonth() + monthNumber() + year() + } + val date = LocalDate(year, month, day) + return format.parse(selectableDates.initialDate) <= date && + format.parse(selectableDates.endDate) >= date +} + private fun isValid(valueString: String) = valueString.length == 8 private fun formatStoredDateToUI(dateValue: String): String? { @@ -175,14 +179,11 @@ private fun formatStoredDateToUI(dateValue: String): String? { } fun formatUIDateToStored(dateValue: String?): InputDateValues? { - return if (dateValue?.length != 8) { + return if (dateValue?.length != 10) { null } else { - val year = dateValue.substring(4, 8).toInt() - val month = dateValue.substring(2, 4).toInt() - val day = dateValue.substring(0, 2).toInt() - - InputDateValues(day, month, year) + val date = LocalDate.Formats.ISO.parse(dateValue) + InputDateValues(date.dayOfMonth, date.monthNumber, date.year) } } 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 3475546966..3ccebc46dd 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.R import org.dhis2.commons.Constants.ENROLLMENT_STATUS import org.dhis2.commons.Constants.ENROLLMENT_UID @@ -26,13 +27,17 @@ import org.dhis2.commons.Constants.ORG_UNIT import org.dhis2.commons.Constants.PROGRAM_STAGE_UID import org.dhis2.commons.Constants.PROGRAM_UID import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.date.toUiStringResource +import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.ui.PeriodSelectorContent import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.EventDetailsFragmentBinding import org.dhis2.maps.views.MapSelectorActivity +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo @@ -52,7 +57,6 @@ import org.dhis2.usescases.general.FragmentGlobalAbstract import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.period.PeriodType -import java.util.Date import javax.inject.Inject class EventDetailsFragment : FragmentGlobalAbstract() { @@ -160,17 +164,11 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } } - viewModel.showPeriods = { - showPeriodDialog() - } + viewModel.showPeriods = ::showPeriodDialog - viewModel.showOrgUnits = { - showOrgUnitDialog() - } + viewModel.showOrgUnits = ::showOrgUnitDialog - viewModel.showNoOrgUnits = { - showNoOrgUnitsDialog() - } + viewModel.showNoOrgUnits = ::showNoOrgUnitsDialog viewModel.requestLocationPermissions = { requestLocationPermissions.launch( @@ -253,7 +251,11 @@ class EventDetailsFragment : FragmentGlobalAbstract() { uiModel = EventInputDateUiModel( eventDate = date, detailsEnabled = details.enabled, - onDateClick = { showPeriodDialog() }, + onDateClick = { + viewModel.getPeriodType()?.let { + showPeriodDialog(it) + } + }, onDateSelected = {}, onClear = { viewModel.onClearEventReportDate() }, required = true, @@ -313,15 +315,28 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } } - private fun showPeriodDialog() { - PeriodDialog() - .setPeriod(viewModel.eventDate.value.periodType) - .setMinDate(viewModel.eventDate.value.minDate) - .setMaxDate(viewModel.eventDate.value.maxDate) - .setPossitiveListener { selectedDate: Date -> - viewModel.setUpEventReportDate(selectedDate) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + private fun showPeriodDialog(periodType: PeriodType) { + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = getString(periodType.toUiStringResource()), + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { _ -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + viewModel.setUpEventReportDate(selectedDate) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } private fun showOrgUnitDialog() { 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 a740d4c70d..cf0e092ff4 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 @@ -6,7 +6,7 @@ import androidx.compose.runtime.getValue 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.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @ExperimentalAnimationApi @BindingAdapter("setReopen") @@ -15,7 +15,7 @@ fun ComposeView.setReopenButton(viewModel: EventDetailsViewModel) { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, ) - MdcTheme { + DHIS2Theme { val eventDetail by viewModel.eventDetails.collectAsState() ReopenButton(eventDetail.canReopen) { viewModel.onReopenClick() } } 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 3c4f1c9e8b..e316aad1c0 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 @@ -2,13 +2,16 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import org.dhis2.commons.extensions.truncate import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.periods.model.Period import org.dhis2.form.data.GeometryController import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo @@ -16,6 +19,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates @@ -49,9 +53,10 @@ class EventDetailsViewModel( private val locationProvider: LocationProvider, private val createOrUpdateEventDetails: CreateOrUpdateEventDetails, private val resourcesProvider: EventDetailResourcesProvider, + private val configurePeriodSelector: ConfigurePeriodSelector, ) : ViewModel() { - var showPeriods: (() -> Unit)? = null + var showPeriods: ((periodType: PeriodType) -> Unit)? = null var showOrgUnits: (() -> Unit)? = null var showNoOrgUnits: (() -> Unit)? = null var requestLocationPermissions: (() -> Unit)? = null @@ -238,7 +243,7 @@ class EventDetailsViewModel( fun showPeriodDialog() { periodType?.let { - showPeriods?.invoke() + showPeriods?.invoke(it) } } @@ -348,6 +353,10 @@ class EventDetailsViewModel( fun cancelCoordinateRequest() { setUpCoordinates(value = eventCoordinates.value.model?.value) } + + fun fetchPeriods(): Flow> { + return configurePeriodSelector() + } } inline fun Result.mockSafeFold( diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt index 5910ee7d71..ebb45a9fec 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt @@ -9,11 +9,11 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.hisp.dhis.android.core.period.PeriodType -@Suppress("UNCHECKED_CAST") class EventDetailsViewModelFactory( private val configureEventDetails: ConfigureEventDetails, private val configureEventReportDate: ConfigureEventReportDate, @@ -26,6 +26,7 @@ class EventDetailsViewModelFactory( private val locationProvider: LocationProvider, private val createOrUpdateEventDetails: CreateOrUpdateEventDetails, private val eventDetailResourcesProvider: EventDetailResourcesProvider, + private val configurePeriodSelector: ConfigurePeriodSelector, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -41,6 +42,7 @@ class EventDetailsViewModelFactory( locationProvider, createOrUpdateEventDetails, eventDetailResourcesProvider, + configurePeriodSelector, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt b/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt index fecc90d4d5..f7d6bf8fb4 100644 --- a/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityOptionsCompat -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject @@ -20,6 +19,7 @@ import org.dhis2.commons.ActivityResultObserver import org.dhis2.commons.Constants import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.service.SessionManagerServiceImpl +import org.dhis2.commons.ui.extensions.handleInsets import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.server.OpenIdSession.LogOutReason import org.dhis2.data.service.SyncStatusController @@ -115,6 +115,8 @@ abstract class SessionManagerActivity : AppCompatActivity(), ActivityResultObser } } + handleInsets() + super.onCreate(savedInstanceState) } @@ -131,7 +133,7 @@ abstract class SessionManagerActivity : AppCompatActivity(), ActivityResultObser override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) { if (activityResultObserver != null) { @@ -190,9 +192,9 @@ abstract class SessionManagerActivity : AppCompatActivity(), ActivityResultObser if (finishAll) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) if (bundle != null) intent.putExtras(bundle) if (transition != null) { - ContextCompat.startActivity(this, intent, transition.toBundle()) + startActivity(intent, transition.toBundle()) } else { - ContextCompat.startActivity(this, intent, null) + startActivity(intent, null) } if (finishCurrent) finish() } @@ -212,7 +214,11 @@ abstract class SessionManagerActivity : AppCompatActivity(), ActivityResultObser } private fun checkSessionTimeout() { - if (::sessionManagerServiceImpl.isInitialized && sessionManagerServiceImpl.checkSessionTimeout({ accountsCount -> sessionAction(accountsCount) }, lifecycleScope) && this !is LoginActivity) { + if (::sessionManagerServiceImpl.isInitialized && sessionManagerServiceImpl.checkSessionTimeout( + { accountsCount -> sessionAction(accountsCount) }, + lifecycleScope, + ) && this !is LoginActivity + ) { workManagerController.cancelAllWork() syncStatusController.restore() } diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt index a1570e4382..cbb3c97114 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.databinding.DataBindingUtil -import com.google.android.material.composethemeadapter.MdcTheme import com.google.gson.Gson import com.google.gson.reflect.TypeToken import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -64,6 +63,7 @@ import org.dhis2.utils.analytics.FORGOT_CODE import org.dhis2.utils.session.PIN_DIALOG_TAG import org.dhis2.utils.session.PinDialog import org.hisp.dhis.android.core.user.openid.IntentWithRequestCode +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import timber.log.Timber import java.io.BufferedReader import java.io.File @@ -202,7 +202,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.topbar.setContent { val displayMoreActions by presenter.displayMoreActions().observeAsState(true) - MdcTheme { + DHIS2Theme { LoginTopBar( version = buildInfo(), displayMoreActions = displayMoreActions, diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt index cca1514537..4fa24a8447 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt @@ -8,13 +8,13 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.livedata.observeAsState -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.bindings.app import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.LoginActivity import org.dhis2.usescases.login.accounts.ui.AccountsScreen +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -63,7 +63,7 @@ class AccountsActivity : ActivityGlobalAbstract() { app().serverComponent()?.plus(AccountsModule())?.inject(this) setContent { - MdcTheme { + DHIS2Theme { val accounts = viewModel.accounts.observeAsState(listOf()) AccountsScreen( accounts = accounts.value, diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModelFactory.kt index 587c9b6214..a63c7594c4 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModelFactory.kt @@ -3,7 +3,6 @@ package org.dhis2.usescases.login.accounts import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -@Suppress("UNCHECKED_CAST") class AccountsViewModelFactory( val repository: AccountRepository, ) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt index fabcf741ae..473b4f5411 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -45,60 +44,58 @@ fun AccountsScreen( onAddAccountClicked: () -> Unit, onImportDatabase: () -> Unit, ) { - MaterialTheme { + Column( + Modifier + .fillMaxWidth() + .background(colorResource(id = R.color.colorPrimary)), + ) { + LoginTopBar( + version = LocalContext.current.buildInfo(), + onImportDatabase = onImportDatabase, + ) + Column( - Modifier - .fillMaxWidth() - .background(colorResource(id = R.color.colorPrimary)), + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxHeight() + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background(Color.White), ) { - LoginTopBar( - version = LocalContext.current.buildInfo(), - onImportDatabase = onImportDatabase, - ) - - Column( - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxHeight() - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) - .background(Color.White), + LazyColumn( + Modifier + .weight(1f) + .padding(top = 16.dp), ) { - LazyColumn( - Modifier - .weight(1f) - .padding(top = 16.dp), - ) { - items(accounts) { - AccountItem( - Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - it, - onAccountClicked, - ) - } + items(accounts) { + AccountItem( + Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + it, + onAccountClicked, + ) } - Column(Modifier.padding(16.dp)) { - Button( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.colorPrimary), - contentColor = Color.White, - ), - elevation = ButtonDefaults.elevation( - defaultElevation = 5.dp, - pressedElevation = 15.dp, - disabledElevation = 0.dp, + } + Column(Modifier.padding(16.dp)) { + Button( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.colorPrimary), + contentColor = Color.White, + ), + elevation = ButtonDefaults.elevation( + defaultElevation = 5.dp, + pressedElevation = 15.dp, + disabledElevation = 0.dp, + ), + onClick = { onAddAccountClicked() }, + ) { + Text( + text = stringResource(R.string.add_accout).toUpperCase(Locale.current), + fontFamily = FontFamily( + Font(R.font.rubik_regular, FontWeight.Medium), ), - onClick = { onAddAccountClicked() }, - ) { - Text( - text = stringResource(R.string.add_accout).toUpperCase(Locale.current), - fontFamily = FontFamily( - Font(R.font.rubik_regular, FontWeight.Medium), - ), - ) - } + ) } } } diff --git a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt index 0e0bf01898..5282d89e54 100644 --- a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt @@ -14,10 +14,10 @@ import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,13 +30,14 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import org.dhis2.R -import org.hisp.dhis.mobile.ui.designsystem.resource.provideFontResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -52,7 +53,7 @@ fun LoginTopBar( modifier = Modifier .fillMaxWidth() .height(80.dp) - .background(MaterialTheme.colors.primary), + .background(MaterialTheme.colorScheme.primary), ) { val (logoLayout, versionLabel) = createRefs() @@ -90,7 +91,7 @@ fun LoginTopBar( Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "More options", - tint = MaterialTheme.colors.onPrimary, + tint = MaterialTheme.colorScheme.onPrimary, ) } @@ -110,7 +111,7 @@ fun LoginTopBar( Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_import_db), contentDescription = "Import database", - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colorScheme.primary, ) Text( @@ -118,7 +119,7 @@ fun LoginTopBar( style = TextStyle( fontSize = 16.sp, lineHeight = 24.sp, - fontFamily = provideFontResource("rubik_regular"), + fontFamily = FontFamily(Font(R.font.rubik_regular)), fontWeight = FontWeight.Normal, color = Color.Black, letterSpacing = 0.5.sp, @@ -140,7 +141,7 @@ fun LoginTopBar( style = TextStyle( fontSize = 12.sp, lineHeight = 16.sp, - fontFamily = provideFontResource("rubik_regular"), + fontFamily = FontFamily(Font(R.font.rubik_regular)), fontWeight = FontWeight.Normal, color = SurfaceColor.ContainerHighest, letterSpacing = 0.4.sp, 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 4ce0756b92..8176bc9d96 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt @@ -9,6 +9,8 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings +import android.transition.ChangeBounds +import android.transition.TransitionManager import android.view.View import android.webkit.MimeTypeMap import android.widget.TextView @@ -21,12 +23,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.NotificationCompat import androidx.databinding.DataBindingUtil import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import dispatch.core.dispatcherProvider import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.R @@ -34,6 +38,11 @@ import org.dhis2.bindings.app import org.dhis2.bindings.hasPermissions import org.dhis2.commons.animations.hide import org.dhis2.commons.animations.show +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.FilterItem +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityMainBinding @@ -67,11 +76,15 @@ class MainActivity : DrawerLayout.DrawerListener { private lateinit var binding: ActivityMainBinding + lateinit var mainComponent: MainComponent @Inject lateinit var presenter: MainPresenter + @Inject + lateinit var newAdapter: FiltersAdapter + @Inject lateinit var pageConfigurator: NavigationPageConfigurator @@ -82,6 +95,8 @@ class MainActivity : // no-op } + private var backDropActive = false + private val requestWritePermissions = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -93,13 +108,7 @@ class MainActivity : private var isPinLayoutVisible = false - private val mainNavigator = MainNavigator( - supportFragmentManager, - { /*no-op*/ }, - ) { titleRes, _, showBottomNavigation -> - setTitle(getString(titleRes)) - setBottomNavigationVisibility(showBottomNavigation) - } + private lateinit var mainNavigator: MainNavigator private val navigationLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { } @@ -138,6 +147,19 @@ class MainActivity : ) mainComponent.inject(this@MainActivity) } ?: navigateTo(true) + mainNavigator = MainNavigator( + dispatcherProvider = presenter.dispatcherProvider, + supportFragmentManager, + { + if (backDropActive) { + showHideFilter() + } + }, + ) { titleRes, showFilterButton, showBottomNavigation -> + setTitle(getString(titleRes)) + setFilterButtonVisibility(showFilterButton) + setBottomNavigationVisibility(showBottomNavigation) + } super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) @@ -154,6 +176,8 @@ class MainActivity : binding.mainDrawerLayout.addDrawerListener(this) + binding.filterRecycler.adapter = newAdapter + setUpNavigationBar() setUpDevelopmentMode() @@ -215,6 +239,8 @@ class MainActivity : super.onResume() if (sessionManagerServiceImpl.isUserLoggedIn()) { presenter.init() + presenter.initFilters() + binding.totalFilters = FilterManager.getInstance().totalFilters } } @@ -286,10 +312,12 @@ class MainActivity : when (it.running) { true -> { binding.syncActionButton.visibility = View.GONE + setFilterButtonVisibility(false) setBottomNavigationVisibility(false) } false -> { + setFilterButtonVisibility(true) binding.syncActionButton.visibility = View.VISIBLE setBottomNavigationVisibility(true) presenter.onDataSuccess() @@ -326,6 +354,35 @@ class MainActivity : } } + override fun showHideFilter() { + val transition = ChangeBounds() + transition.duration = 200 + TransitionManager.beginDelayedTransition(binding.backdropLayout, transition) + backDropActive = !backDropActive + val initSet = ConstraintSet() + initSet.clone(binding.backdropLayout) + if (backDropActive) { + initSet.connect( + R.id.fragment_container, + ConstraintSet.TOP, + R.id.filterRecycler, + ConstraintSet.BOTTOM, + 50, + ) + binding.navigationBar.hide() + } else { + initSet.connect( + R.id.fragment_container, + ConstraintSet.TOP, + R.id.toolbar, + ConstraintSet.BOTTOM, + 0, + ) + binding.navigationBar.show() + } + initSet.applyTo(binding.backdropLayout) + } + override fun showGranularSync() { SyncStatusDialog.Builder() .withContext(this) @@ -350,6 +407,37 @@ class MainActivity : .show("ALL_SYNC") } + override fun updateFilters(totalFilters: Int) { + binding.totalFilters = totalFilters + } + + override fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) { + if (periodRequest == FilterManager.PeriodRequest.FROM_TO) { + DateUtils.getInstance() + .fromCalendarSelector(this) { FilterManager.getInstance().addPeriod(it) } + } else { + DateUtils.getInstance() + .showPeriodDialog( + this, + { datePeriods -> FilterManager.getInstance().addPeriod(datePeriods) }, + true, + { FilterManager.getInstance().addPeriod(null) }, + ) + } + } + + override fun openOrgUnitTreeSelector() { + OUTreeFragment.Builder() + .withPreselectedOrgUnits( + FilterManager.getInstance().orgUnitFilters.map { it.uid() }.toMutableList(), + ) + .onSelection { selectedOrgUnits -> + presenter.setOrgUnitFilters(selectedOrgUnits) + } + .build() + .show(supportFragmentManager, "OUTreeFragment") + } + override fun goToLogin(accountsCount: Int, isDeletion: Boolean) { startActivity( LoginActivity::class.java, @@ -370,6 +458,19 @@ class MainActivity : binding.executePendingBindings() } + private fun setFilterButtonVisibility(showFilterButton: Boolean) { + binding.filterActionButton.visibility = if (showFilterButton && presenter.hasFilters()) { + View.VISIBLE + } else { + View.GONE + } + binding.syncActionButton.visibility = if (showFilterButton) { + View.VISIBLE + } else { + View.GONE + } + } + override fun openDrawer(gravity: Int) { if (!binding.mainDrawerLayout.isDrawerOpen(gravity)) { binding.mainDrawerLayout.openDrawer(gravity) @@ -378,6 +479,14 @@ class MainActivity : } } + override fun setFilters(filters: List) { + newAdapter.submitList(filters) + } + + override fun hideFilters() { + binding.filterActionButton.visibility = View.GONE + } + override fun onLockClick() { if (!presenter.isPinStored()) { binding.mainDrawerLayout.closeDrawers() @@ -422,9 +531,11 @@ class MainActivity : } override fun onDrawerStateChanged(newState: Int) { + // no op } override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + // no op } override fun onDrawerClosed(drawerView: View) { @@ -432,6 +543,7 @@ class MainActivity : } override fun onDrawerOpened(drawerView: View) { + // no op } private fun initCurrentScreen() { diff --git a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt index 62014e272b..e8bf1c7a24 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt @@ -6,6 +6,8 @@ import dhis2.org.analytics.charts.Charts import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils @@ -32,6 +34,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { preferences: PreferenceProvider, workManagerController: WorkManagerController, filterManager: FilterManager, + filterRepository: FilterRepository, matomoAnalyticsController: MatomoAnalyticsController, userManager: UserManager, deleteUserData: DeleteUserData, @@ -47,6 +50,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { preferences, workManagerController, filterManager, + filterRepository, matomoAnalyticsController, userManager, deleteUserData, @@ -74,6 +78,12 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { return HomeRepositoryImpl(d2, charts, featureConfigRepositoryImpl) } + @Provides + @PerActivity + fun provideNewFiltersAdapter(): FiltersAdapter { + return FiltersAdapter() + } + @Provides @PerActivity fun providePageConfigurator( diff --git a/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt b/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt index 8a3fa8cd72..272a051ff9 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt @@ -10,6 +10,9 @@ import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.dhis2.R import org.dhis2.usescases.about.AboutFragment import org.dhis2.usescases.main.program.ProgramFragment @@ -18,6 +21,7 @@ import org.dhis2.usescases.settings.SyncManagerFragment import org.dhis2.usescases.troubleshooting.TroubleshootingFragment class MainNavigator( + private val dispatcherProvider: dispatch.core.DispatcherProvider, private val fragmentManager: FragmentManager, private val onTransitionStart: () -> Unit, private val onScreenChanged: ( @@ -133,38 +137,50 @@ class MainNavigator( onTransitionStart() currentScreen.value = screen currentFragment = fragment - val transaction: FragmentTransaction = fragmentManager.beginTransaction() - transaction.apply { - if (sharedView == null) { - val (enterAnimation, exitAnimation) = if (useFadeInTransition) { - Pair(android.R.anim.fade_in, android.R.anim.fade_out) - } else { - Pair(R.anim.fragment_enter_right, R.anim.fragment_exit_left) - } - val (enterPopAnimation, exitPopAnimation) = if (useFadeInTransition) { - Pair(android.R.anim.fade_in, android.R.anim.fade_out) - } else { - Pair(R.anim.fragment_enter_left, R.anim.fragment_exit_right) + + CoroutineScope(dispatcherProvider.main).launch { + withContext(dispatcherProvider.io) { + val transaction: FragmentTransaction = fragmentManager.beginTransaction() + transaction.apply { + if (sharedView == null) { + val (enterAnimation, exitAnimation) = getEnterExitAnimation(useFadeInTransition) + val (enterPopAnimation, exitPopAnimation) = getEnterExitPopAnimation(useFadeInTransition) + setCustomAnimations( + enterAnimation, + exitAnimation, + enterPopAnimation, + exitPopAnimation, + ) + } else { + setReorderingAllowed(true) + addSharedElement(sharedView, "contenttest") + } } - setCustomAnimations( - enterAnimation, - exitAnimation, - enterPopAnimation, - exitPopAnimation, - ) - } else { - setReorderingAllowed(true) - addSharedElement(sharedView, "contenttest") + .replace(R.id.fragment_container, fragment, fragment::class.simpleName) + .commitAllowingStateLoss() } + onScreenChanged( + screen.title, + isPrograms(), + isHome(), + ) } - .replace(R.id.fragment_container, fragment, fragment::class.simpleName) - .commitAllowingStateLoss() - - onScreenChanged( - screen.title, - isPrograms(), - isHome(), - ) + } + } + + private fun getEnterExitPopAnimation(useFadeInTransition: Boolean): Pair { + return if (useFadeInTransition) { + Pair(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + Pair(R.anim.fragment_enter_left, R.anim.fragment_exit_right) + } + } + + private fun getEnterExitAnimation(useFadeInTransition: Boolean): Pair { + return if (useFadeInTransition) { + Pair(android.R.anim.fade_in, android.R.anim.fade_out) + } else { + Pair(R.anim.fragment_enter_right, R.anim.fragment_exit_left) } } } 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 bc88aa6a72..a4147ee408 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.work.ExistingWorkPolicy import io.reactivex.Completable +import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.commons.Constants import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.Actions.Companion.BLOCK_SESSION_PIN import org.dhis2.commons.matomo.Actions.Companion.OPEN_ANALYTICS import org.dhis2.commons.matomo.Actions.Companion.QR_SCANNER @@ -40,6 +42,7 @@ import org.dhis2.usescases.login.SyncIsPerformedInteractor import org.dhis2.usescases.settings.DeleteUserData import org.dhis2.usescases.sync.WAS_INITIAL_SYNC_DONE import org.dhis2.utils.TRUE +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.systeminfo.SystemInfo import org.hisp.dhis.android.core.user.User import timber.log.Timber @@ -57,6 +60,7 @@ class MainPresenter( private val preferences: PreferenceProvider, private val workManagerController: WorkManagerController, private val filterManager: FilterManager, + private val filterRepository: FilterRepository, private val matomoAnalyticsController: MatomoAnalyticsController, private val userManager: UserManager, private val deleteUserData: DeleteUserData, @@ -77,7 +81,6 @@ class MainPresenter( val downloadingVersion = MutableLiveData(false) fun init() { - filterManager.clearAllFilters() preferences.removeValue(Preference.CURRENT_ORG_UNIT) disposable.add( repository.user() @@ -117,6 +120,54 @@ class MainPresenter( trackDhis2Server() } + fun initFilters() { + disposable.add( + Flowable.just(filterRepository.homeFilters()) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { filters -> + if (filters.isEmpty()) { + view.hideFilters() + } else { + view.setFilters(filters) + } + }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.asFlowable() + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { filterManager -> view.updateFilters(filterManager.totalFilters) }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.periodRequest + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { periodRequest -> view.showPeriodRequest(periodRequest.first) }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.ouTreeFlowable() + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { view.openOrgUnitTreeSelector() }, + { Timber.e(it) }, + ), + ) + } + fun trackDhis2Server() { disposable.add( repository.getServerVersion() @@ -145,6 +196,10 @@ class MainPresenter( } } + fun setOrgUnitFilters(selectedOrgUnits: List) { + filterManager.addOrgUnits(selectedOrgUnits) + } + private fun getUserUid(): String { return try { userManager.d2.userModule().user().blockingGet()?.uid() ?: "" @@ -201,6 +256,10 @@ class MainPresenter( view.back() } + fun showFilter() { + view.showHideFilter() + } + fun onDetach() { disposable.clear() } @@ -219,6 +278,7 @@ class MainPresenter( fun onNavigateBackToHome() { view.goToHome() + initFilters() } fun onClickSyncManager() { @@ -307,4 +367,8 @@ class MainPresenter( fun getSingleItemData(): HomeItemData? { return repository.singleHomeItemData() } + + fun hasFilters(): Boolean { + return filterRepository.homeFilters().isNotEmpty() + } } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainView.kt b/app/src/main/java/org/dhis2/usescases/main/MainView.kt index 1d590f3c84..7f518d138a 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainView.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainView.kt @@ -26,6 +26,8 @@ package org.dhis2.usescases.main import androidx.annotation.UiThread +import org.dhis2.commons.filters.FilterItem +import org.dhis2.commons.filters.FilterManager import org.dhis2.usescases.general.AbstractActivityContracts import java.io.File @@ -36,12 +38,24 @@ interface MainView : AbstractActivityContracts.View { fun openDrawer(gravity: Int) + fun showHideFilter() + fun onLockClick() fun changeFragment(id: Int) + fun updateFilters(totalFilters: Int) + + fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) + + fun openOrgUnitTreeSelector() + fun goToHome() + fun setFilters(filters: List) + + fun hideFilters() + fun showGranularSync() fun goToLogin(accountsCount: Int, isDeletion: Boolean) diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt index 8f22319228..e4bb21670b 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.resources.ColorUtils @@ -12,6 +13,7 @@ import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusController import org.hisp.dhis.android.core.D2 @@ -25,7 +27,9 @@ class ProgramModule(private val view: ProgramView) { dispatcherProvider: DispatcherProvider, featureConfigRepository: FeatureConfigRepository, matomoAnalyticsController: MatomoAnalyticsController, + filterManager: FilterManager, syncStatusController: SyncStatusController, + schedulerProvider: SchedulerProvider, ): ProgramViewModelFactory { return ProgramViewModelFactory( view, @@ -33,7 +37,9 @@ class ProgramModule(private val view: ProgramView) { featureConfigRepository, dispatcherProvider, matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) } @@ -43,6 +49,7 @@ class ProgramModule(private val view: ProgramView) { d2: D2, filterPresenter: FilterPresenter, dhisProgramUtils: DhisProgramUtils, + dhisTrackedEntityInstanceUtils: DhisTrackedEntityInstanceUtils, schedulerProvider: SchedulerProvider, colorUtils: ColorUtils, metadataIconProvider: MetadataIconProvider, @@ -51,6 +58,7 @@ class ProgramModule(private val view: ProgramView) { d2, filterPresenter, dhisProgramUtils, + dhisTrackedEntityInstanceUtils, ResourceManager(view.context, colorUtils), metadataIconProvider, schedulerProvider, 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 e19c0ca348..a9fdacf122 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 @@ -9,6 +9,7 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusData import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.State @@ -21,6 +22,7 @@ internal class ProgramRepositoryImpl( private val d2: D2, private val filterPresenter: FilterPresenter, private val dhisProgramUtils: DhisProgramUtils, + private val dhisTeiUtils: DhisTrackedEntityInstanceUtils, private val resourceManager: ResourceManager, private val metadataIconProvider: MetadataIconProvider, private val schedulerProvider: SchedulerProvider, @@ -67,8 +69,13 @@ internal class ProgramRepositoryImpl( programViewModelMapper.map( dataSet, it, - it.dataSetInstanceCount(), + if (filterPresenter.isAssignedToMeApplied()) { + 0 + } else { + it.dataSetInstanceCount() + }, resourceManager.defaultDataSetLabel(), + filterPresenter.areFiltersActive(), metadataIconProvider(dataSet.style(), SurfaceColor.Primary), ) } @@ -107,6 +114,8 @@ internal class ProgramRepositoryImpl( 0, recordLabel, state, + hasOverdue = false, + filtersAreActive = false, metadataIconData = metadataIconProvider(program.style(), SurfaceColor.Primary), ).copy( stockConfig = if (d2.isStockProgram(program.uid())) { @@ -121,15 +130,19 @@ internal class ProgramRepositoryImpl( private fun List.applyFilters(): List { return map { programModel -> val program = d2.programModule().programs().uid(programModel.uid).blockingGet() - val count = + val (count, hasOverdue) = if (program?.programType() == WITHOUT_REGISTRATION) { getSingleEventCount(program) } else if (program?.programType() == WITH_REGISTRATION) { getTrackerTeiCount(program) } else { - 0 + Pair(0, false) } - programModel.copy(count = count) + programModel.copy( + count = count, + hasOverdueEvent = hasOverdue, + filtersAreActive = filterPresenter.areFiltersActive(), + ) } } @@ -156,13 +169,20 @@ internal class ProgramRepositoryImpl( } } - private fun getSingleEventCount(program: Program): Int { - return filterPresenter.filteredEventProgram(program) - .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size + private fun getSingleEventCount(program: Program): Pair { + return Pair( + filterPresenter.filteredEventProgram(program) + .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size, + false, + ) } - private fun getTrackerTeiCount(program: Program): Int { - return filterPresenter.filteredTrackerProgram(program) - .offlineFirst().blockingCount() + private fun getTrackerTeiCount(program: Program): Pair { + val teiIds = filterPresenter.filteredTrackerProgram(program) + .offlineFirst().blockingGetUids() + val mCount = teiIds.size + val mOverdue = dhisTeiUtils.hasOverdueInProgram(teiIds, program) + + return Pair(mCount, mOverdue) } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt index bb2ac6334d..aea8ee904c 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle @@ -56,8 +55,6 @@ import androidx.compose.ui.zIndex import org.dhis2.R import org.dhis2.commons.bindings.addIf import org.dhis2.commons.date.toDateSpan -import org.dhis2.commons.resources.ColorType -import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.ui.icons.toIconData import org.dhis2.data.service.SyncStatusData import org.dhis2.ui.MetadataIconData @@ -271,19 +268,6 @@ fun StateIcon( } } -@Composable -fun DownloadingProgress() { - CircularProgressIndicator( - modifier = Modifier - .size(24.dp) - .padding(2.dp), - color = Color( - ColorUtils().getPrimaryColor(LocalContext.current, ColorType.PRIMARY), - ), - strokeWidth = 2.dp, - ) -} - @Composable fun DownloadedIcon() { Icon( @@ -349,7 +333,7 @@ fun DownloadMedia() { fontFamily = FontFamily(Font(R.font.rubik_regular)), ), ) - DownloadingProgress() + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) } } } @@ -682,6 +666,8 @@ private fun testingProgramModel() = ProgramUiModel( onlyEnrollOnce = false, accessDataWrite = true, state = State.SYNCED, + hasOverdueEvent = false, + filtersAreActive = false, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt index c1440bcf30..ff19b3a8f1 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt @@ -17,6 +17,8 @@ data class ProgramUiModel( val onlyEnrollOnce: Boolean, val accessDataWrite: Boolean, val state: State, + val hasOverdueEvent: Boolean, + val filtersAreActive: Boolean, val downloadState: ProgramDownloadState, val downloadActive: Boolean = false, val stockConfig: AppConfig?, diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt index 26f010bb11..df757fc112 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt @@ -5,18 +5,22 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.reactivex.disposables.CompositeDisposable +import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.featureconfig.model.Feature import org.dhis2.commons.featureconfig.model.FeatureOptions +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.Actions.Companion.SYNC_BTN import org.dhis2.commons.matomo.Categories.Companion.HOME import org.dhis2.commons.matomo.Labels.Companion.CLICK_ON import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.SyncStatusController import timber.log.Timber +import java.util.concurrent.TimeUnit class ProgramViewModel internal constructor( private val view: ProgramView, @@ -24,17 +28,60 @@ class ProgramViewModel internal constructor( private val featureConfigRepository: FeatureConfigRepository, private val dispatchers: DispatcherProvider, private val matomoAnalyticsController: MatomoAnalyticsController, + private val filterManager: FilterManager, private val syncStatusController: SyncStatusController, + private val schedulerProvider: SchedulerProvider, ) : ViewModel() { private val _programs = MutableLiveData>() val programs: LiveData> = _programs - + private val refreshData = PublishProcessor.create() var disposable: CompositeDisposable = CompositeDisposable() fun init() { programRepository.clearCache() fetchPrograms() + initFilters() + } + + private fun initFilters() { + val applyFilter = PublishProcessor.create() + disposable.add( + applyFilter + .switchMap { + refreshData.debounce( + 500, + TimeUnit.MILLISECONDS, + schedulerProvider.io(), + ).startWith(Unit).switchMap { + programRepository.homeItems( + syncStatusController.observeDownloadProcess().value, + ) + } + } + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { programs -> + _programs.postValue(programs) + }, + { throwable -> Timber.d(throwable) }, + { Timber.tag("INIT DATA").d("LOADING ENDED") }, + ), + ) + + disposable.add( + filterManager.asFlowable() + .startWith(filterManager) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { + applyFilter.onNext(filterManager) + }, + { Timber.e(it) }, + ), + ) } private fun fetchPrograms() { diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt index f302ba11bf..2d4caab8f1 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt @@ -3,18 +3,21 @@ package org.dhis2.usescases.main.program import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.SyncStatusController -@Suppress("UNCHECKED_CAST") class ProgramViewModelFactory( private val view: ProgramView, private val programRepository: ProgramRepository, private val featureConfigRepository: FeatureConfigRepository, private val dispatchers: DispatcherProvider, private val matomoAnalyticsController: MatomoAnalyticsController, + private val filterManager: FilterManager, private val syncStatusController: SyncStatusController, + private val schedulerProvider: SchedulerProvider, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ProgramViewModel( @@ -23,7 +26,9 @@ class ProgramViewModelFactory( featureConfigRepository, dispatchers, matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt index a632bfdf50..dce4e8f6a5 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt @@ -13,6 +13,8 @@ class ProgramViewModelMapper() { recordCount: Int, recordLabel: String, state: State, + hasOverdue: Boolean, + filtersAreActive: Boolean, metadataIconData: MetadataIconData, ): ProgramUiModel { return ProgramUiModel( @@ -31,6 +33,8 @@ class ProgramViewModelMapper() { onlyEnrollOnce = program.onlyEnrollOnce() == true, accessDataWrite = program.access().data().write(), state = State.valueOf(state.name), + hasOverdueEvent = hasOverdue, + filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = program.lastUpdated() ?: Date(), @@ -42,6 +46,7 @@ class ProgramViewModelMapper() { dataSetInstanceSummary: DataSetInstanceSummary, recordCount: Int, dataSetLabel: String, + filtersAreActive: Boolean, metadataIconData: MetadataIconData, ): ProgramUiModel { return ProgramUiModel( @@ -56,6 +61,8 @@ class ProgramViewModelMapper() { onlyEnrollOnce = false, accessDataWrite = dataSet.access().data().write(), state = dataSetInstanceSummary.state(), + hasOverdueEvent = false, + filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = dataSet.lastUpdated() ?: Date(), diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index 3fac0ea471..6fc126bfb0 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -7,31 +7,18 @@ import android.transition.ChangeBounds import android.transition.Transition import android.transition.TransitionManager import android.view.View +import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.constraintlayout.widget.ConstraintSet -import androidx.databinding.DataBindingUtil import androidx.lifecycle.viewModelScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.dhis2.R -import org.dhis2.bindings.app import org.dhis2.bindings.clipWithRoundedCorners import org.dhis2.bindings.dp +import org.dhis2.bindings.userComponent import org.dhis2.commons.Constants import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.DateUtils.OnFromToSelector @@ -60,12 +47,10 @@ import org.dhis2.utils.analytics.DATA_CREATION import org.dhis2.utils.category.CategoryDialog import org.dhis2.utils.category.CategoryDialog.Companion.TAG import org.dhis2.utils.customviews.RxDateDialog -import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import org.hisp.dhis.android.core.period.DatePeriod import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import timber.log.Timber import java.util.Date @@ -110,17 +95,22 @@ class ProgramEventDetailActivity : initInjection() themeManager?.setProgramTheme(programUid) super.onCreate(savedInstanceState) - initEventFilters() - initViewModel() - binding = DataBindingUtil.setContentView(this, R.layout.activity_program_event_detail) - binding.presenter = presenter - binding.totalFilters = FilterManager.getInstance().totalFilters - setupBottomNavigation() - binding.fragmentContainer.clipWithRoundedCorners(16.dp) - binding.filterLayout.adapter = filtersAdapter - presenter.init() - binding.syncButton.setOnClickListener { showSyncDialogProgram() } + setContent { + DHIS2Theme { + ProgramEventDetailScreen( + programEventsViewModel, + presenter, + networkUtils, + { binding = it }, + { + initBindings() + initEventFilters() + initViewModel() + }, + ) + } + } if (intent.shouldLaunchSyncDialog()) { showSyncDialogProgram() @@ -140,65 +130,13 @@ class ProgramEventDetailActivity : } } - private fun setupBottomNavigation() { - binding.navigationBar.setContent { - DHIS2Theme { - val uiState by programEventsViewModel.navigationBarUIState - val isBackdropActive by programEventsViewModel.backdropActive.observeAsState(false) - var selectedItemIndex by remember(uiState) { - mutableIntStateOf( - uiState.items.indexOfFirst { - it.id == uiState.selectedItem - }, - ) - } - - LaunchedEffect(uiState.selectedItem) { - when (uiState.selectedItem) { - NavigationPage.LIST_VIEW -> { - programEventsViewModel.showList() - } - - NavigationPage.MAP_VIEW -> { - networkUtils.performIfOnline( - context = this@ProgramEventDetailActivity, - action = { - presenter.trackEventProgramMap() - programEventsViewModel.showMap() - }, - onDialogDismissed = { - selectedItemIndex = 0 - }, - noNetworkMessage = getString(R.string.msg_network_connection_maps), - ) - } - - NavigationPage.ANALYTICS -> { - presenter.trackEventProgramAnalytics() - programEventsViewModel.showAnalytics() - } - - else -> { - // no-op - } - } - } - - AnimatedVisibility( - visible = uiState.items.size > 1 && isBackdropActive.not(), - enter = slideInVertically(animationSpec = tween(200)) { it }, - exit = slideOutVertically(animationSpec = tween(200)) { it }, - ) { - NavigationBar( - modifier = Modifier.fillMaxWidth(), - items = uiState.items, - selectedItemIndex = selectedItemIndex, - ) { page -> - programEventsViewModel.onNavigationPageChanged(page) - } - } - } - } + private fun initBindings() { + binding.presenter = presenter + binding.totalFilters = FilterManager.getInstance().totalFilters + binding.fragmentContainer.clipWithRoundedCorners(16.dp) + binding.filterLayout.adapter = filtersAdapter + binding.syncButton.setOnClickListener { showSyncDialogProgram() } + binding.totalFilters = FilterManager.getInstance().totalFilters } private fun initExtras() { @@ -206,7 +144,7 @@ class ProgramEventDetailActivity : } private fun initInjection() { - component = app().userComponent() + component = userComponent() ?.plus( ProgramEventDetailModule( this, @@ -243,9 +181,7 @@ class ProgramEventDetailActivity : programEventsViewModel.onRecreationActivity(false) } } - programEventsViewModel.writePermission.observe(this) { canWrite: Boolean -> - binding.addEventButton.visibility = if (canWrite) View.VISIBLE else View.GONE - } + programEventsViewModel.currentScreen.observe(this) { currentScreen: EventProgramScreen? -> currentScreen?.let { when (it) { @@ -257,12 +193,6 @@ class ProgramEventDetailActivity : } } - override fun onResume() { - super.onResume() - binding.addEventButton.isEnabled = true - binding.totalFilters = FilterManager.getInstance().totalFilters - } - private fun showSyncDialogProgram() { SyncStatusDialog.Builder() .withContext(this) @@ -273,11 +203,9 @@ class ProgramEventDetailActivity : } }) .onNoConnectionListener { - Snackbar.make( - binding.root, - R.string.sync_offline_check_connection, - Snackbar.LENGTH_SHORT, - ).show() + programEventsViewModel.displayMessage( + getString(R.string.sync_offline_check_connection), + ) } .show("EVENT_SYNC") } @@ -367,13 +295,6 @@ class ProgramEventDetailActivity : ConstraintSet.BOTTOM, 0, ) - initSet.connect( - R.id.addEventButton, - ConstraintSet.BOTTOM, - R.id.fragmentContainer, - ConstraintSet.BOTTOM, - 16.dp, - ) } else { initSet.connect( R.id.fragmentContainer, @@ -389,13 +310,6 @@ class ProgramEventDetailActivity : ConstraintSet.TOP, 0, ) - initSet.connect( - R.id.addEventButton, - ConstraintSet.BOTTOM, - R.id.navigationBar, - ConstraintSet.TOP, - 16.dp, - ) } initSet.applyTo(binding.backdropLayout) } @@ -428,8 +342,6 @@ class ProgramEventDetailActivity : programStageUid = it, ) } - } else { - enableAddEventButton(true) } } .build() @@ -437,10 +349,6 @@ class ProgramEventDetailActivity : } } - private fun enableAddEventButton(enable: Boolean) { - binding.addEventButton.isEnabled = enable - } - override fun setWritePermission(canWrite: Boolean) { programEventsViewModel.writePermission.value = canWrite } @@ -523,46 +431,36 @@ class ProgramEventDetailActivity : } }) .onNoConnectionListener { - Snackbar.make( - binding.root, - R.string.sync_offline_check_connection, - Snackbar.LENGTH_SHORT, - ).show() + programEventsViewModel.displayMessage( + getString(R.string.sync_offline_check_connection), + ) } .show(FRAGMENT_TAG) } private fun showList() { supportFragmentManager.beginTransaction().replace( - R.id.fragmentContainer, + binding.fragmentContainer.id, EventListFragment(), "EVENT_LIST", ).commitNow() - binding.addEventButton.visibility = - if (programEventsViewModel.writePermission.value == true) { - View.VISIBLE - } else { - View.GONE - } binding.filter.visibility = View.VISIBLE } private fun showMap() { supportFragmentManager.beginTransaction().replace( - R.id.fragmentContainer, + binding.fragmentContainer.id, EventMapFragment(), "EVENT_MAP", ).commitNow() - binding.addEventButton.visibility = View.GONE binding.filter.visibility = View.VISIBLE } private fun showAnalytics() { supportFragmentManager.beginTransaction().replace( - R.id.fragmentContainer, + binding.fragmentContainer.id, GroupAnalyticsFragment.forProgram(programUid), ).commitNow() - binding.addEventButton.visibility = View.GONE binding.filter.visibility = View.GONE } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt index e95fb9b5b3..d61a575874 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt @@ -139,12 +139,14 @@ class ProgramEventDetailModule( resourceManager: ResourceManager, metadataIconProvider: MetadataIconProvider, profilePictureProvider: ProfilePictureProvider, + dateUtils: DateUtils, ) = EventInfoProvider( d2, resourceManager, DateLabelProvider(context, resourceManager), metadataIconProvider, profilePictureProvider, + dateUtils, ) @Provides diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt index 4300aecc9a..83824e9eaa 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt @@ -164,6 +164,6 @@ class ProgramEventDetailRepositoryImpl internal constructor( override fun displayOrganisationUnit(programUid: String): Boolean { return d2.organisationUnitModule().organisationUnits() .byProgramUids(listOf(programUid)) - .blockingGet().size > 1 + .blockingCount() > 1 } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailScreen.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailScreen.kt new file mode 100644 index 0000000000..d590ccbbce --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailScreen.kt @@ -0,0 +1,179 @@ +package org.dhis2.usescases.programEventDetail + +import android.view.LayoutInflater +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.viewinterop.AndroidView +import org.dhis2.R +import org.dhis2.commons.network.NetworkUtils +import org.dhis2.databinding.ActivityProgramEventDetailBinding +import org.dhis2.model.SnackbarMessage +import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel.EventProgramScreen +import org.dhis2.utils.customviews.navigationbar.NavigationPage +import org.hisp.dhis.mobile.ui.designsystem.component.FAB +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.dropShadow + +@Composable +fun ProgramEventDetailScreen( + programEventsViewModel: ProgramEventDetailViewModel, + presenter: ProgramEventDetailPresenter, + networkUtils: NetworkUtils, + onBindingReady: (ActivityProgramEventDetailBinding) -> Unit, + onViewReady: () -> Unit, +) { + val context = LocalContext.current + val isBackdropActive by programEventsViewModel.backdropActive.observeAsState(false) + val snackbarHostState = remember { SnackbarHostState() } + val snackbarMessage by programEventsViewModel.snackbarMessage.collectAsState(SnackbarMessage()) + + LaunchedEffect(snackbarMessage) { + if (snackbarMessage.message.isNotEmpty()) { + snackbarHostState.showSnackbar(snackbarMessage.message) + } + } + + Scaffold( + snackbarHost = { + SnackbarHost(snackbarHostState) { data -> + Snackbar( + modifier = Modifier.dropShadow(shape = SnackbarDefaults.shape), + snackbarData = data, + containerColor = SurfaceColor.SurfaceBright, + contentColor = TextColor.OnSurface, + ) + } + }, + floatingActionButton = { + val writePermission by programEventsViewModel.writePermission.observeAsState( + false, + ) + val currentScreen by programEventsViewModel.currentScreen.observeAsState() + val displayFAB by remember { + derivedStateOf { + when (currentScreen) { + EventProgramScreen.LIST -> true + else -> false + } && writePermission && + isBackdropActive.not() + } + } + AnimatedVisibility( + visible = displayFAB, + enter = scaleIn(), + exit = scaleOut(), + ) { + FAB( + modifier = Modifier.testTag("ADD_EVENT_BUTTON"), + onClick = presenter::addEvent, + icon = { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "add event", + tint = TextColor.OnPrimary, + ) + }, + ) + } + }, + bottomBar = { + val uiState by programEventsViewModel.navigationBarUIState + + var selectedItemIndex by remember(uiState) { + mutableIntStateOf( + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + }, + ) + } + + LaunchedEffect(uiState.selectedItem) { + when (uiState.selectedItem) { + NavigationPage.LIST_VIEW -> { + programEventsViewModel.showList() + } + + NavigationPage.MAP_VIEW -> { + networkUtils.performIfOnline( + context = context, + action = { + presenter.trackEventProgramMap() + programEventsViewModel.showMap() + }, + onDialogDismissed = { + selectedItemIndex = 0 + }, + noNetworkMessage = context.getString(R.string.msg_network_connection_maps), + ) + } + + NavigationPage.ANALYTICS -> { + presenter.trackEventProgramAnalytics() + programEventsViewModel.showAnalytics() + } + + else -> { + // no-op + } + } + } + + AnimatedVisibility( + visible = uiState.items.size > 1 && isBackdropActive.not(), + enter = slideInVertically(animationSpec = tween(200)) { it }, + exit = slideOutVertically(animationSpec = tween(200)) { it }, + ) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + items = uiState.items, + selectedItemIndex = selectedItemIndex, + onItemClick = programEventsViewModel::onNavigationPageChanged, + ) + } + }, + ) { + AndroidView( + modifier = Modifier + .fillMaxSize() + .padding(it), + factory = { context -> + ActivityProgramEventDetailBinding.inflate( + LayoutInflater.from(context), + ).also(onBindingReady).root + }, + update = { + onViewReady() + presenter.init() + }, + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt index 425587c116..be1235dbee 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt @@ -16,12 +16,14 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import org.dhis2.R import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.layer.basemaps.BaseMapStyle import org.dhis2.maps.usecases.MapStyleConfiguration +import org.dhis2.model.SnackbarMessage import org.dhis2.tracker.NavigationBarUIState import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPage @@ -63,6 +65,9 @@ class ProgramEventDetailViewModel( private val _navigationBarUIState = mutableStateOf(NavigationBarUIState()) val navigationBarUIState: State> = _navigationBarUIState + private val _snackbarMessage = MutableSharedFlow() + val snackbarMessage = _snackbarMessage.asSharedFlow() + init { viewModelScope.launch { loadBottomBarItems() } } @@ -167,4 +172,10 @@ class ProgramEventDetailViewModel( } } } + + fun displayMessage(msg: String) { + viewModelScope.launch(dispatcher.io()) { + _snackbarMessage.emit(SnackbarMessage(message = msg)) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt index 0b0d55bf23..e01567f088 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt @@ -8,7 +8,6 @@ import org.dhis2.maps.usecases.MapStyleConfiguration import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator -@Suppress("UNCHECKED_CAST") class ProgramEventDetailViewModelFactory( private val mapStyleConfiguration: MapStyleConfiguration, private val eventRepository: ProgramEventDetailRepository, diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt index 61271eccb1..36c2023b0b 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt @@ -221,6 +221,7 @@ class EventMapFragment : } override fun onDestroy() { + programEventsViewModel.setProgress(false) presenter.onDestroy() super.onDestroy() } diff --git a/app/src/main/java/org/dhis2/usescases/qrReader/QrReaderPresenterImpl.java b/app/src/main/java/org/dhis2/usescases/qrReader/QrReaderPresenterImpl.java index c4cce0782d..bb62b7b8b1 100644 --- a/app/src/main/java/org/dhis2/usescases/qrReader/QrReaderPresenterImpl.java +++ b/app/src/main/java/org/dhis2/usescases/qrReader/QrReaderPresenterImpl.java @@ -2,10 +2,10 @@ import android.util.Log; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.schedulers.SchedulerProvider; import org.dhis2.commons.data.tuples.Pair; import org.dhis2.commons.data.tuples.Trio; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.common.FeatureType; import org.hisp.dhis.android.core.common.Geometry; diff --git a/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt b/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt index e78cbbc67f..a42c5517d5 100644 --- a/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt @@ -93,7 +93,7 @@ class ScanActivity : ActivityGlobalAbstract() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt index 63bfae26bd..687c32c375 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchJavaToCompose.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.usescases.searchTrackEntity.ui.WrappedSearchButton +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @ExperimentalAnimationApi fun ComposeView?.setLandscapeOpenSearchButton( @@ -16,7 +16,7 @@ fun ComposeView?.setLandscapeOpenSearchButton( onClick: () -> Unit, ) { this?.setContent { - MdcTheme { + DHIS2Theme { val screenState by searchTEIViewModel.screenState.observeAsState() val teTypeName by searchTEIViewModel.teTypeName.observeAsState() 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 7aecc48978..8c89241eb2 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -33,6 +33,7 @@ import org.dhis2.metadata.usecases.FileResourceConfiguration; import org.dhis2.metadata.usecases.ProgramConfiguration; import org.dhis2.metadata.usecases.TrackedEntityInstanceConfiguration; +import org.dhis2.tracker.data.ProfilePictureProvider; import org.dhis2.tracker.relationships.model.RelationshipDirection; import org.dhis2.tracker.relationships.model.RelationshipModel; import org.dhis2.tracker.relationships.model.RelationshipOwnerType; @@ -61,7 +62,6 @@ import org.hisp.dhis.android.core.relationship.Relationship; 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; @@ -121,6 +121,7 @@ public class SearchRepositoryImpl implements SearchRepository { private HashMap> trackedEntityTypeAttributesUidsCache = new HashMap(); private final MetadataIconProvider metadataIconProvider; + private final ProfilePictureProvider profilePictureProvider; SearchRepositoryImpl(String teiType, @Nullable String initialProgram, @@ -134,7 +135,8 @@ public class SearchRepositoryImpl implements SearchRepository { NetworkUtils networkUtils, SearchTEIRepository searchTEIRepository, ThemeManager themeManager, - MetadataIconProvider metadataIconProvider + MetadataIconProvider metadataIconProvider, + ProfilePictureProvider profilePictureProvider ) { this.teiType = teiType; this.d2 = d2; @@ -155,6 +157,7 @@ public class SearchRepositoryImpl implements SearchRepository { currentProgram, resources); this.metadataIconProvider = metadataIconProvider; + this.profilePictureProvider = profilePictureProvider; } @@ -389,39 +392,17 @@ private void setOverdueEvents(@NonNull SearchTeiModel tei, Program selectedProgr String teiId = tei.getTei() != null && tei.getTei().uid() != null ? tei.getTei().uid() : ""; List enrollments = d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiId).blockingGet(); - EventCollectionRepository scheduledEvents = d2.eventModule().events().byEnrollmentUid().in(UidsHelper.getUidsList(enrollments)) - .byStatus().eq(EventStatus.SCHEDULE) - .byDueDate().beforeOrEqual(new Date()); - EventCollectionRepository overdueEvents = d2.eventModule().events().byEnrollmentUid().in(UidsHelper.getUidsList(enrollments)).byStatus().eq(EventStatus.OVERDUE); if (selectedProgram != null) { - scheduledEvents = scheduledEvents.byProgramUid().eq(selectedProgram.uid()).orderByDueDate(RepositoryScope.OrderByDirection.DESC); - overdueEvents = overdueEvents.byProgramUid().eq(selectedProgram.uid()).orderByDueDate(RepositoryScope.OrderByDirection.DESC); + overdueEvents = overdueEvents.byProgramUid().eq(selectedProgram.uid()); } - int count; - List scheduleList = scheduledEvents.blockingGet(); - List overdueList = overdueEvents.blockingGet(); - count = overdueList.size() + scheduleList.size(); + List overdueList = overdueEvents.orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet(); - if (count > 0) { + if (!overdueList.isEmpty()) { tei.setHasOverdue(true); - Date scheduleDate = !scheduleList.isEmpty() ? scheduleList.get(0).dueDate() : null; - Date overdueDate = !overdueList.isEmpty() ? overdueList.get(0).dueDate() : null; - Date dateToShow = null; - if (scheduleDate != null && overdueDate != null) { - if (scheduleDate.before(overdueDate)) { - dateToShow = overdueDate; - } else { - dateToShow = scheduleDate; - } - } else if (scheduleDate != null) { - dateToShow = scheduleDate; - } else if (overdueDate != null) { - dateToShow = overdueDate; - } - tei.setOverdueDate(dateToShow); + tei.setOverdueDate(overdueList.get(0).dueDate()); } } @@ -436,9 +417,6 @@ private void setRelationshipsInfo(@NonNull SearchTeiModel searchTeiModel, Progra ); for (Relationship relationship : relationships) { if (relationship.from().trackedEntityInstance() != null) { - RelationshipType relationshipType = - d2.relationshipModule().relationshipTypes().uid(relationship.relationshipType()).blockingGet(); - String relationshipTEIUid; RelationshipDirection direction; if (!searchTeiModel.getTei().uid().equals(relationship.from().trackedEntityInstance().trackedEntityInstance())) { @@ -469,7 +447,6 @@ private void setRelationshipsInfo(@NonNull SearchTeiModel searchTeiModel, Progra relationship, fromTei.geometry(), toTei.geometry(), - relationshipType, direction, relationshipTEIUid, RelationshipOwnerType.TEI, @@ -768,7 +745,7 @@ public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Pr } else { searchTei.setEnrolledOrgUnit(orgUnitName(searchTei.getTei().organisationUnit())); } - searchTei.setProfilePicture(profilePicturePath(dbTei, selectedProgram)); + searchTei.setProfilePicture(profilePictureProvider.invoke(dbTei, selectedProgram != null ? selectedProgram.uid() : null)); } else { searchTei.setTei(teiFromItem); searchTei.setEnrolledOrgUnit(orgUnitName(searchTei.getTei().organisationUnit())); diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt index 0a813a057b..7447735efd 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt @@ -460,7 +460,7 @@ class SearchTEActivity : ActivityGlobalAbstract(), SearchTEContractsModule.View currentContent = Content.LIST supportFragmentManager.beginTransaction().run { replace(R.id.mainComponent, get(fromRelationship)) - commit() + commitAllowingStateLoss() } hideToolbarProgressBar() } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java index 1dec62ca98..ca9c9907fc 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java @@ -155,6 +155,7 @@ SearchRepository searchRepository(@NonNull D2 d2, SearchTEIRepository searchTEIRepository, ThemeManager themeManager, MetadataIconProvider metadataIconProvider) { + ProfilePictureProvider profilePictureProvider = new ProfilePictureProvider(d2); return new SearchRepositoryImpl(teiType, initialProgram, d2, @@ -167,7 +168,8 @@ SearchRepository searchRepository(@NonNull D2 d2, networkUtils, searchTEIRepository, themeManager, - metadataIconProvider); + metadataIconProvider, + profilePictureProvider); } @Provides @@ -178,7 +180,8 @@ SearchRepositoryKt searchRepositoryKt( DispatcherProvider dispatcherProvider, FieldViewModelFactory fieldViewModelFactory, MetadataIconProvider metadataIconProvider, - ColorUtils colorUtils + ColorUtils colorUtils, + DateUtils dateUtils ) { ResourceManager resourceManager = new ResourceManager(moduleContext, colorUtils); DateLabelProvider dateLabelProvider = new DateLabelProvider(moduleContext, new ResourceManager(moduleContext, colorUtils)); @@ -201,7 +204,8 @@ SearchRepositoryKt searchRepositoryKt( resourceManager, dateLabelProvider, metadataIconProvider, - profilePictureProvider + profilePictureProvider, + dateUtils ) ); } @@ -323,6 +327,13 @@ SearchTeiViewModelFactory providesViewModelFactory( ); } + @Provides + @PerActivity + DateUtils provideDateUtils( + ) { + return DateUtils.getInstance(); + } + @Provides @PerActivity ProgramConfigurationRepository provideProgramConfigurationRepository( diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt index ea0a6e9f7a..524bfcc16b 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiViewModelFactory.kt @@ -9,7 +9,6 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.ui.provider.DisplayNameProvider import org.dhis2.maps.usecases.MapStyleConfiguration -@Suppress("UNCHECKED_CAST") class SearchTeiViewModelFactory( private val searchRepository: SearchRepository, private val searchRepositoryKt: SearchRepositoryKt, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchResultHolder.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchResultHolder.kt index e1395e63e8..84f635b82e 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchResultHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchResultHolder.kt @@ -3,10 +3,10 @@ package org.dhis2.usescases.searchTrackEntity.listView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.bindings.dp import org.dhis2.databinding.ResultSearchListBinding import org.dhis2.usescases.searchTrackEntity.ui.SearchResultUi +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme class SearchResultHolder( val binding: ResultSearchListBinding, @@ -27,7 +27,7 @@ class SearchResultHolder( } } }.setContent { - MdcTheme { + DHIS2Theme { SearchResultUi( searchResult = item, onSearchOutsideClick = onSearchOutsideProgram, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt index c601bb59a7..08280dd3ba 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt @@ -241,7 +241,7 @@ class SearchTEMap : FragmentGlobalAbstract() { ), actionButton = { SyncButtonProvider(state = item.state) { - presenter.onSyncIconClick(item.uid) + presenter.onSyncIconClick(item.relatedInfo?.enrollment?.uid) } }, onCardClick = { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt index 174ca0bd22..2e8b189115 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt @@ -24,13 +24,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.OutlinedButton import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -61,6 +60,8 @@ import org.dhis2.usescases.searchTrackEntity.listView.SearchResult import org.hisp.dhis.mobile.ui.designsystem.component.ExtendedFAB import org.hisp.dhis.mobile.ui.designsystem.component.FAB import org.hisp.dhis.mobile.ui.designsystem.component.FABStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType import org.hisp.dhis.mobile.ui.designsystem.component.SearchBar import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2TextStyle import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing @@ -198,7 +199,7 @@ fun SearchButtonWithQuery( .clickable( onClick = onClick, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( + indication = ripple( true, color = SurfaceColor.Primary, ), @@ -333,7 +334,7 @@ fun LoadingContent(loadingDescription: String) { .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - CircularProgressIndicator() + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) Spacer(modifier = Modifier.size(16.dp)) Text( text = loadingDescription, 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 28c270ed10..9be82c3530 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import org.dhis2.R import org.dhis2.bindings.hasFollowUp +import org.dhis2.commons.bindings.isFilePathValid import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager @@ -70,7 +71,7 @@ class TEICardMapper( null } - if (item.profilePicturePath.isNotEmpty()) { + if (isFilePathValid(item.profilePicturePath)) { val file = File(item.profilePicturePath) val bitmap = BitmapFactory.decodeFile(file.absolutePath).asImageBitmap() val painter = BitmapPainter(bitmap) diff --git a/app/src/main/java/org/dhis2/usescases/settings/ErrorViewHolder.java b/app/src/main/java/org/dhis2/usescases/settings/ErrorViewHolder.java index 5b898a0a9e..790ad7af41 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ErrorViewHolder.java +++ b/app/src/main/java/org/dhis2/usescases/settings/ErrorViewHolder.java @@ -5,9 +5,9 @@ import androidx.recyclerview.widget.RecyclerView; import org.dhis2.commons.data.tuples.Pair; +import org.dhis2.commons.date.DateUtils; import org.dhis2.databinding.ItemErrorDialogBinding; import org.dhis2.usescases.settings.models.ErrorViewModel; -import org.dhis2.utils.DateUtils; import io.reactivex.processors.FlowableProcessor; diff --git a/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt b/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt index 4c1aafd7f3..b72fa3e2ba 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt @@ -1,11 +1,17 @@ package org.dhis2.usescases.settings.bindings +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.platform.ComposeView import androidx.databinding.BindingAdapter -import org.dhis2.ui.Dhis2ProgressIndicator import org.dhis2.ui.model.ButtonUiModel import org.dhis2.ui.theme.Dhis2Theme +import org.dhis2.ui.theme.textSecondary import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @BindingAdapter("addTextButton") fun ComposeView.addTextButton(model: ButtonUiModel?) { @@ -25,8 +31,13 @@ fun ComposeView.addTextButton(model: ButtonUiModel?) { @BindingAdapter("progressIndicator") fun ComposeView.progressIndicator(message: String?) { setContent { - Dhis2Theme { - Dhis2ProgressIndicator(message) + DHIS2Theme { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + ProgressIndicator( + type = ProgressIndicatorType.CIRCULAR, + ) + message?.let { Text(it, color = textSecondary) } + } } } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index a972e26f35..b9c1faea4a 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -23,7 +23,7 @@ import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.tracker.NavigationBarUIState import org.dhis2.tracker.TEIDashboardItems -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState import org.dhis2.utils.AuthorityException import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt index 285951d4ef..424da8e62e 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt @@ -7,7 +7,6 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator -@Suppress("UNCHECKED_CAST") class DashboardViewModelFactory( val repository: DashboardRepository, val analyticsHelper: AnalyticsHelper, 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 db58f8aba3..812b36fd34 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -26,6 +26,7 @@ import androidx.databinding.DataBindingUtil import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import org.dhis2.App @@ -47,7 +48,7 @@ import org.dhis2.databinding.ActivityDashboardMobileBinding import org.dhis2.form.model.EnrollmentMode import org.dhis2.form.ui.provider.FormResultDialogProvider import org.dhis2.tracker.TEIDashboardItems -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState import org.dhis2.ui.ThemeManager import org.dhis2.ui.dialogs.bottomsheet.DeleteBottomSheetDialog import org.dhis2.usescases.enrollment.DateEditionWarningHandler @@ -335,7 +336,7 @@ class TeiDashboardMobileActivity : private fun setUpNavigationBar() { binding.navigationBar.setContent { DHIS2Theme { - val uiState by dashboardViewModel.navigationBarUIState.collectAsState() + val uiState by dashboardViewModel.navigationBarUIState.collectAsStateWithLifecycle() var selectedHomeItemIndex by remember(uiState) { mutableIntStateOf( uiState.items.indexOfFirst { @@ -396,7 +397,7 @@ class TeiDashboardMobileActivity : supportFragmentManager.beginTransaction() .replace(R.id.fragmentContainer, fragment, item.name) - .commit() + .commitAllowingStateLoss() updateTopBar(item) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt index 0ed182698b..9cbe67e899 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt @@ -1,7 +1,7 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships import androidx.lifecycle.LiveData -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState interface MapButtonObservable { fun relationshipMap(): LiveData diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt index a2799ef335..6f689f42c7 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt @@ -41,11 +41,12 @@ import org.dhis2.maps.managers.RelationshipMapManager import org.dhis2.maps.views.LocationIcon import org.dhis2.maps.views.MapScreen import org.dhis2.maps.views.OnMapClickListener -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState import org.dhis2.tracker.relationships.ui.DeleteRelationshipsConfirmation import org.dhis2.tracker.relationships.ui.RelationShipsScreen -import org.dhis2.tracker.relationships.ui.RelationshipsUiState import org.dhis2.tracker.relationships.ui.RelationshipsViewModel +import org.dhis2.tracker.relationships.ui.state.RelationshipSectionUiState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipsUiState import org.dhis2.ui.ThemeManager import org.dhis2.ui.avatar.AvatarProvider import org.dhis2.ui.theme.Dhis2Theme @@ -53,7 +54,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureAc import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.dhis2.utils.OnDialogClickListener -import org.hisp.dhis.android.core.relationship.RelationshipType import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.IconButton import org.hisp.dhis.mobile.ui.designsystem.component.IconButtonStyle @@ -81,7 +81,7 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView { @Inject lateinit var relationShipsViewModel: RelationshipsViewModel - private var relationshipType: RelationshipType? = null + private var relationshipSection: RelationshipSectionUiState? = null private var relationshipMapManager: RelationshipMapManager? = null private lateinit var mapButtonObservable: MapButtonObservable @@ -92,7 +92,13 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView { } is RelationshipResult.Success -> { - presenter.addRelationship(it.teiUidToAddAsRelationship, relationshipType!!.uid()) + relationshipSection?.let { relationshipSection -> + relationShipsViewModel.onAddRelationship( + selectedTeiUid = it.teiUidToAddAsRelationship, + relationshipTypeUid = relationshipSection.uid, + relationshipSide = relationshipSection.side, + ) + } } } } @@ -141,12 +147,11 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView { uiState = uiState, relationshipSelectionState = relationshipSelectionState, onCreateRelationshipClick = { - it.teiTypeUid?.let { teiTypeUid -> - goToRelationShip( - relationshipTypeModel = it.relationshipType, - teiTypeUid = teiTypeUid, - ) - } + relationshipSection = it + presenter.goToAddRelationship( + it.uid, + it.entityToAdd, + ) }, onRelationshipClick = { presenter.onRelationshipClicked( @@ -402,11 +407,6 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView { ) } - private fun goToRelationShip(relationshipTypeModel: RelationshipType, teiTypeUid: String) { - relationshipType = relationshipTypeModel - presenter.goToAddRelationship(teiTypeUid, relationshipType!!) - } - override fun showPermissionError() { displayMessage(getString(R.string.search_access_error)) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java index 119258cb81..e84005bbc9 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java @@ -4,7 +4,10 @@ import org.dhis2.commons.data.ProgramConfigurationRepository; import org.dhis2.commons.date.DateLabelProvider; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.di.dagger.PerFragment; +import org.dhis2.commons.network.NetworkUtils; +import org.dhis2.commons.resources.D2ErrorUtils; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.viewmodel.DispatcherProvider; @@ -18,9 +21,11 @@ import org.dhis2.tracker.relationships.data.EventRelationshipsRepository; import org.dhis2.tracker.relationships.data.RelationshipsRepository; import org.dhis2.tracker.relationships.data.TrackerRelationshipsRepository; +import org.dhis2.tracker.relationships.domain.AddRelationship; import org.dhis2.tracker.relationships.domain.DeleteRelationships; import org.dhis2.tracker.relationships.domain.GetRelationshipsByType; import org.dhis2.tracker.relationships.ui.RelationshipsViewModel; +import org.dhis2.tracker.relationships.ui.mapper.RelationshipsUiStateMapper; import org.dhis2.tracker.ui.AvatarProvider; import org.dhis2.usescases.events.EventInfoProvider; import org.dhis2.usescases.teiDashboard.TeiAttributesProvider; @@ -96,7 +101,8 @@ RelationshipMapsRepository providesRepository( D2 d2, ResourceManager resourceManager, MetadataIconProvider metadataIconProvider, - DateLabelProvider dateLabelProvider + DateLabelProvider dateLabelProvider, + DateUtils dateUtils ) { RelationshipConfiguration config; if (teiUid != null) { @@ -119,7 +125,8 @@ RelationshipMapsRepository providesRepository( resourceManager, dateLabelProvider, metadataIconProvider, - profilePictureProvider + profilePictureProvider, + dateUtils ) ); } @@ -146,26 +153,37 @@ TeiAttributesProvider teiAttributesProvider(D2 d2) { RelationshipsViewModel provideRelationshipsViewModel( GetRelationshipsByType getRelationshipsByType, DeleteRelationships deleteRelationships, - DispatcherProvider dispatcherProvider + DispatcherProvider dispatcherProvider, + AddRelationship addRelationship, + D2ErrorUtils d2ErrorUtils, + RelationshipsUiStateMapper relationshipsUiStateMapper ) { return new RelationshipsViewModel( + dispatcherProvider, getRelationshipsByType, deleteRelationships, - dispatcherProvider + addRelationship, + d2ErrorUtils, + relationshipsUiStateMapper ); } + @Provides + @PerFragment + DateUtils provideDateUtils( + ) { + return DateUtils.getInstance(); + } + @Provides @PerFragment GetRelationshipsByType provideGetRelationshipsByType( RelationshipsRepository relationshipsRepository, - DateLabelProvider dateLabelProvider, - AvatarProvider avatarProvider + DispatcherProvider dispatcherProvider ) { return new GetRelationshipsByType( relationshipsRepository, - dateLabelProvider, - avatarProvider + dispatcherProvider ); } @@ -177,6 +195,15 @@ DeleteRelationships provideDeleteRelationships( return new DeleteRelationships(relationshipsRepository); } + @Provides + @PerFragment + AddRelationship provideAddRelationship( + DispatcherProvider dispatcherProvider, + RelationshipsRepository relationshipsRepository + ) { + return new AddRelationship(dispatcherProvider, relationshipsRepository); + } + @Provides @PerFragment RelationshipsRepository provideRelationshipsRepository( @@ -222,4 +249,21 @@ AvatarProvider provideAvatarProvider( ) { return new AvatarProvider(metadataIconProvider); } + + @Provides + @PerFragment + D2ErrorUtils provideD2ErrorUtils( + NetworkUtils networkUtils + ) { + return new D2ErrorUtils(moduleContext, networkUtils); + } + + @Provides + @PerFragment + RelationshipsUiStateMapper provideRelationshipsUiStateMapper( + AvatarProvider avatarProvider, + DateLabelProvider dateLabelProvider + ) { + return new RelationshipsUiStateMapper(avatarProvider, dateLabelProvider); + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt index b1f1db0065..e50fccf3cb 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt @@ -24,15 +24,10 @@ import org.dhis2.tracker.relationships.model.RelationshipOwnerType import org.dhis2.tracker.ui.AvatarProvider import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.CLICK -import org.dhis2.utils.analytics.DELETE_RELATIONSHIP import org.dhis2.utils.analytics.NEW_RELATIONSHIP import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.State -import org.hisp.dhis.android.core.maintenance.D2Error -import org.hisp.dhis.android.core.relationship.RelationshipHelper -import org.hisp.dhis.android.core.relationship.RelationshipType import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem -import timber.log.Timber class RelationshipPresenter internal constructor( private val view: RelationshipView, @@ -126,81 +121,28 @@ class RelationshipPresenter internal constructor( } } - fun goToAddRelationship(teiTypeToAdd: String, relationshipType: RelationshipType) { - val writeAccess = - d2.relationshipModule().relationshipService().hasAccessPermission(relationshipType) + fun goToAddRelationship( + relationshipTypeUid: String, + teiTypeToAdd: String?, + ) { + val writeAccess = relationshipsRepository.hasWritePermission(relationshipTypeUid) if (writeAccess) { analyticsHelper.setEvent(NEW_RELATIONSHIP, CLICK, NEW_RELATIONSHIP) - if (teiUid != null) { - view.goToAddRelationship(teiUid, teiTypeToAdd) - } else if (eventUid != null) { - view.goToAddRelationship(eventUid, teiTypeToAdd) + + val originUid = teiUid ?: eventUid + + if (originUid != null && teiTypeToAdd != null) { + view.goToAddRelationship(originUid, teiTypeToAdd) } } else { view.showPermissionError() } } - fun deleteRelationship(relationshipUid: String) { - try { - d2.relationshipModule().relationships().withItems().uid(relationshipUid) - .blockingDelete() - } catch (e: D2Error) { - Timber.d(e) - } finally { - analyticsHelper.setEvent(DELETE_RELATIONSHIP, CLICK, DELETE_RELATIONSHIP) - updateRelationships.onNext(true) - } - } - - fun addRelationship(selectedTei: String, relationshipTypeUid: String) { - if (teiUid != null) { - addTeiToTeiRelationship(teiUid, selectedTei, relationshipTypeUid) - } else if (eventUid != null) { - addEventToTeiRelationship(eventUid, selectedTei, relationshipTypeUid) - } - } - - private fun addTeiToTeiRelationship( - teiUid: String, - selectedTei: String, - relationshipTypeUid: String, - ) { - try { - val relationship = - RelationshipHelper.teiToTeiRelationship(teiUid, selectedTei, relationshipTypeUid) - d2.relationshipModule().relationships().blockingAdd(relationship) - } catch (e: D2Error) { - view.displayMessage(e.errorDescription()) - } finally { - updateRelationships.onNext(true) - } - } - - private fun addEventToTeiRelationship( - eventUid: String, - selectedTei: String, - relationshipTypeUid: String, - ) { - try { - val relationship = - RelationshipHelper.eventToTeiRelationship( - eventUid, - selectedTei, - relationshipTypeUid, - ) - d2.relationshipModule().relationships().blockingAdd(relationship) - } catch (e: D2Error) { - view.displayMessage(e.errorDescription()) - } finally { - updateRelationships.onNext(true) - } - } - fun openDashboard(teiUid: String) { if (d2.trackedEntityModule() - .trackedEntityInstances().uid(teiUid).blockingGet()!!.state() != + .trackedEntityInstances().uid(teiUid).blockingGet()?.aggregatedSyncState() != State.RELATIONSHIP ) { if (d2.enrollmentModule().enrollments() @@ -221,7 +163,7 @@ class RelationshipPresenter internal constructor( } } - fun openEvent(eventUid: String, eventProgramUid: String) { + private fun openEvent(eventUid: String, eventProgramUid: String) { view.openEventFor(eventUid, eventProgramUid) } 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 657a53312c..c92295d179 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 @@ -603,7 +603,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { ) .onSelection { selectedOrgUnits -> if (selectedOrgUnits.isNotEmpty()) { - presenter.onOrgUnitForNewEventSelected( + presenter.onNewEventSelected( orgUnitUid = selectedOrgUnits.first().uid(), programStageUid = programStageUid, ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt index 4c21b073f5..fa34d5a7d6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt @@ -92,6 +92,7 @@ class TEIDataModule( d2: D2, periodUtils: DhisPeriodUtils, metadataIconProvider: MetadataIconProvider, + dateUtils: DateUtils, ): TeiDataRepository { return TeiDataRepositoryImpl( d2, @@ -100,6 +101,7 @@ class TEIDataModule( enrollmentUid, periodUtils, metadataIconProvider, + dateUtils, ) } @@ -165,8 +167,9 @@ class TEIDataModule( @PerFragment fun providesTEIEventCardMapper( resourceManager: ResourceManager, + dateUtils: DateUtils, ): TEIEventCardMapper { - return TEIEventCardMapper(resourceManager) + return TEIEventCardMapper(resourceManager, dateUtils) } @Provides @@ -191,5 +194,5 @@ class TEIDataModule( fun provideD2ErrorUtils() = D2ErrorUtils(view.context, NetworkUtils(view.context)) @Provides - fun provideDateUtils() = DateUtils.getInstance() + fun provideDateUtils(): DateUtils = DateUtils.getInstance() } 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 c910d79cbe..a5906c722e 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 @@ -367,7 +367,8 @@ class TEIDataPresenter( if (stage != null) { when (eventCreationType) { EventCreationType.ADDNEW -> programUid?.let { program -> - checkOrgUnitCount(program, stage.uid()) + val orgUnitUid = d2.enrollment(enrollmentUid)?.organisationUnit() + orgUnitUid?.let { onNewEventSelected(orgUnitUid, stage.uid()) } ?: checkOrgUnitCount(program, stage.uid()) } EventCreationType.SCHEDULE -> { @@ -425,14 +426,14 @@ class TEIDataPresenter( CoroutineScope(dispatcher.io()).launch { val orgUnits = teiDataRepository.programOrgListInCaptureScope(programUid) if (orgUnits.count() == 1) { - onOrgUnitForNewEventSelected(orgUnits.first().uid(), programStageUid) + onNewEventSelected(orgUnits.first().uid(), programStageUid) } else { view.displayOrgUnitSelectorForNewEvent(programUid, programStageUid) } } } - fun onOrgUnitForNewEventSelected(orgUnitUid: String, programStageUid: String) { + fun onNewEventSelected(orgUnitUid: String, programStageUid: String) { CoroutineScope(dispatcher.io()).launch { programUid?.let { createEventUseCase( 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 7e8227b954..c5897d2486 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 @@ -33,6 +33,7 @@ class TeiDataRepositoryImpl( private val enrollmentUid: String?, private val periodUtils: DhisPeriodUtils, private val metadataIconProvider: MetadataIconProvider, + private val dateUtils: DateUtils, ) : TeiDataRepository { override fun getTEIEnrollmentEvents( @@ -379,7 +380,7 @@ class TeiDataRepositoryImpl( private fun checkEventStatus(events: List): List { return events.mapNotNull { event -> if (event.status() == EventStatus.SCHEDULE && - event.dueDate()?.before(DateUtils.getInstance().today) == true + dateUtils.isEventDueDateOverdue(event.dueDate()) ) { d2.eventModule().events().uid(event.uid()).setStatus(EventStatus.OVERDUE) d2.eventModule().events().uid(event.uid()).blockingGet() diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt index 1cb3755533..abedc14046 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import org.dhis2.R import org.dhis2.commons.data.EventViewModel +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.ui.model.ListCardUiModel @@ -34,6 +35,7 @@ import java.util.Date class TEIEventCardMapper( val resourceManager: ResourceManager, + val dateUtils: DateUtils, ) { fun map( @@ -193,18 +195,19 @@ class TEIEventCardMapper( EventStatus.SCHEDULE -> { val text = dueDate.toOverdueOrScheduledUiText(resourceManager) - + val color = if (dateUtils.isEventDueDateOverdue(dueDate)) AdditionalInfoItemColor.ERROR.color else AdditionalInfoItemColor.SUCCESS.color + val icon = if (dateUtils.isEventDueDateOverdue(dueDate)) Icons.Outlined.EventBusy else Icons.Outlined.Event AdditionalInfoItem( icon = { Icon( - imageVector = Icons.Outlined.Event, + imageVector = icon, contentDescription = text, - tint = AdditionalInfoItemColor.SUCCESS.color, + tint = color, ) }, value = text, isConstantItem = true, - color = AdditionalInfoItemColor.SUCCESS.color, + color = color, ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt index fe1494541b..f9163ebd47 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt @@ -14,17 +14,22 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels +import androidx.paging.compose.collectAsLazyPagingItems import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.parcelize.Parcelize import org.dhis2.bindings.app import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.date.toUiStringResource +import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener +import org.dhis2.commons.periods.ui.PeriodSelectorContent import org.dhis2.form.R import org.dhis2.form.model.EventMode +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import java.util.Date +import org.hisp.dhis.android.core.period.PeriodType import javax.inject.Inject class SchedulingDialog : BottomSheetDialogFragment() { @@ -139,8 +144,8 @@ class SchedulingDialog : BottomSheetDialogFragment() { showCalendarDialog() } - viewModel.showPeriods = { - showPeriodDialog() + viewModel.showPeriods = { periodType -> + showPeriodDialog(periodType) } return ComposeView(requireContext()).apply { @@ -168,6 +173,7 @@ class SchedulingDialog : BottomSheetDialogFragment() { override fun onNegativeClick() { // Unused } + override fun onPositiveClick(datePicker: DatePicker) { viewModel.onDateSet( datePicker.year, @@ -180,15 +186,28 @@ class SchedulingDialog : BottomSheetDialogFragment() { dialog.show() } - private fun showPeriodDialog() { - PeriodDialog() - .setPeriod(viewModel.eventDate.value.periodType) - .setMinDate(viewModel.eventDate.value.minDate) - .setMaxDate(viewModel.eventDate.value.maxDate) - .setPossitiveListener { selectedDate: Date -> - viewModel.setUpEventReportDate(selectedDate) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + private fun showPeriodDialog(periodType: PeriodType) { + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = getString(periodType.toUiStringResource()), + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { _ -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + viewModel.setUpEventReportDate(selectedDate) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } sealed interface LaunchMode : Parcelable { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt index fa5e8cca27..4c65ac5c87 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt @@ -45,7 +45,9 @@ import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonBlock import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import java.util.Locale @Composable fun SchedulingDialogUi( @@ -64,7 +66,7 @@ fun SchedulingDialogUi( it.value, selected = false, enabled = true, - textInput = provideStringResource(it.value), + textInput = provideStringResource(it.value.lowercase(Locale.getDefault())), ) } var optionSelected by remember { mutableStateOf(yesNoOptions.first()) } @@ -138,14 +140,17 @@ private fun ButtonBlock( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier) { + Box( + modifier + .padding(top = Spacing24, bottom = Spacing24, start = Spacing24, end = Spacing24), + ) { when (launchMode) { is LaunchMode.NewSchedule -> { Button( modifier = Modifier.fillMaxWidth(), style = ButtonStyle.FILLED, enabled = !scheduleNew || - !date.dateValue.isNullOrEmpty() && + date.isValid && catCombo.isCompleted, text = buttonTitle(scheduleNew), onClick = { @@ -166,7 +171,7 @@ private fun ButtonBlock( Button( modifier = Modifier.fillMaxWidth(), style = ButtonStyle.FILLED, - enabled = !date.dateValue.isNullOrEmpty(), + enabled = date.isValid, text = stringResource(R.string.enter_event, eventLabel), onClick = { viewModel.enterEvent(launchMode) @@ -272,6 +277,7 @@ fun ProvideScheduleNewEventForm( onDateClick = {}, onDateSelected = { viewModel.onDateSet(it.year, it.month, it.day) }, onClear = { viewModel.onClearEventReportDate() }, + onError = { viewModel.onDateError() }, ), ) } else { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index 9c2070b1d4..fae169fd79 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt @@ -2,9 +2,13 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.dhis2.commons.bindings.enrollment @@ -12,6 +16,8 @@ import org.dhis2.commons.bindings.event import org.dhis2.commons.bindings.programStage import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toOverdueOrScheduledUiText +import org.dhis2.commons.periods.domain.GetEventPeriods +import org.dhis2.commons.periods.model.Period import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager @@ -27,6 +33,7 @@ import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import java.text.SimpleDateFormat @@ -42,6 +49,7 @@ class SchedulingViewModel( private val dispatchersProvider: DispatcherProvider, private val launchMode: LaunchMode, private val dateUtils: DateUtils, + private val getEventPeriods: GetEventPeriods, ) : ViewModel() { lateinit var repository: EventDetailsRepository @@ -49,7 +57,7 @@ class SchedulingViewModel( lateinit var configureEventCatCombo: ConfigureEventCatCombo var showCalendar: (() -> Unit)? = null - var showPeriods: (() -> Unit)? = null + var showPeriods: ((periodType: PeriodType) -> Unit)? = null var onEventScheduled: ((String) -> Unit)? = null var onEventSkipped: ((String?) -> Unit)? = null var onDueDateUpdated: (() -> Unit)? = null @@ -88,7 +96,10 @@ class SchedulingViewModel( val enrollment = withContext(dispatchersProvider.io()) { when (launchMode) { is LaunchMode.NewSchedule -> d2.enrollment(launchMode.enrollmentUid) - is LaunchMode.EnterEvent -> null + is LaunchMode.EnterEvent -> { + val enrollmentUid = d2.event(launchMode.eventUid)?.enrollment() + enrollmentUid?.let { d2.enrollment(it) } + } } } _enrollment.value = enrollment @@ -98,6 +109,7 @@ class SchedulingViewModel( is LaunchMode.NewSchedule -> { launchMode.programStagesUids.mapNotNull(d2::programStage) } + is LaunchMode.EnterEvent -> emptyList() } } @@ -162,6 +174,7 @@ class SchedulingViewModel( resourceManager = resourceManager, eventResourcesProvider = eventResourcesProvider, ) + private fun loadProgramStage(event: Event? = null) { viewModelScope.launch { val selectedDate = event?.dueDate() ?: configureEventReportDate.getNextScheduleDate() @@ -215,10 +228,9 @@ class SchedulingViewModel( d2.eventModule().events().uid(eventUid).run { setDueDate(dueDate.currentDate) setStatus(EventStatus.SCHEDULE) + onDueDateUpdated?.invoke() } } - - onDueDateUpdated?.invoke() } } @@ -229,6 +241,12 @@ class SchedulingViewModel( ) } + fun onDateError() { + _eventDate.update { + it.copy(error = true) + } + } + fun setUpCategoryCombo(categoryOption: Pair? = null) { viewModelScope.launch { configureEventCatCombo(categoryOption) @@ -245,7 +263,7 @@ class SchedulingViewModel( fun showPeriodDialog() { programStage.value?.periodType()?.let { - showPeriods?.invoke() + showPeriods?.invoke(it) } } @@ -304,13 +322,29 @@ class SchedulingViewModel( viewModelScope.launch { when (launchMode) { is LaunchMode.EnterEvent -> { - d2.eventModule().events().uid(launchMode.eventUid).setStatus(EventStatus.SKIPPED) + d2.eventModule().events().uid(launchMode.eventUid) + .setStatus(EventStatus.SKIPPED) onEventSkipped?.invoke(programStage.value?.displayEventLabel()) } + is LaunchMode.NewSchedule -> { // no-op } } } } + + fun fetchPeriods(): Flow> { + val programStage = programStage.value ?: return emptyFlow() + val periodType = programStage.periodType() ?: PeriodType.Daily + val enrollmentUid = enrollment.value?.uid() ?: return emptyFlow() + return getEventPeriods( + eventUid = null, + periodType = periodType, + selectedDate = eventDate.value.currentDate, + programStage = programStage, + isScheduling = true, + eventEnrollmentUid = enrollmentUid, + ) + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt index dbe9523c9a..222ac44364 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt @@ -6,13 +6,13 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.periods.domain.GetEventPeriods import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.hisp.dhis.android.core.D2 -@Suppress("UNCHECKED_CAST") class SchedulingViewModelFactory @AssistedInject constructor( private val d2: D2, private val resourceManager: ResourceManager, @@ -20,6 +20,7 @@ class SchedulingViewModelFactory @AssistedInject constructor( private val periodUtils: DhisPeriodUtils, private val dateUtils: DateUtils, private val dispatcherProvider: DispatcherProvider, + private val getEventPeriods: GetEventPeriods, @Assisted private val launchMode: SchedulingDialog.LaunchMode, ) : ViewModelProvider.Factory { @@ -37,6 +38,7 @@ class SchedulingViewModelFactory @AssistedInject constructor( dateUtils = dateUtils, dispatchersProvider = dispatcherProvider, launchMode = launchMode, + getEventPeriods = getEventPeriods, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java index cca9c72c9b..3cc3cd948c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java @@ -107,6 +107,8 @@ public Flowable> allPrograms(String trackedEntityId) { 0, "", State.SYNCED, + false, + false, metadataIconProvider.invoke(program.style()) ) ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt index a0411cc345..5b3d506d51 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt @@ -113,6 +113,8 @@ private fun testingProgramModel(downloadState: ProgramDownloadState) = ProgramUi downloadState = downloadState, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) const val PROGRAM_TO_ENROLL = "PROGRAM_TO_ENROLL_%s" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt index 63f6f43010..66a671e2d3 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt @@ -1,9 +1,8 @@ package org.dhis2.usescases.teiDashboard.ui import androidx.compose.foundation.layout.Column -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,19 +24,20 @@ import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData @Composable fun NewEventOptions( options: List>, + addButtonTestTag: String = TEST_ADD_EVENT_BUTTON, onOptionSelected: (EventCreationType) -> Unit, ) { var expanded by remember { mutableStateOf(false) } Column { IconButton( - modifier = Modifier.testTag(TEST_ADD_EVENT_BUTTON), + modifier = Modifier.testTag(addButtonTestTag), style = IconButtonStyle.FILLED, icon = { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_add_accent), contentDescription = "New event", - tint = MaterialTheme.colors.onPrimary, + tint = Color.White, ) }, onClick = { expanded = !expanded }, @@ -71,6 +71,4 @@ fun NewEventOptionsPreview() { } } -data class EventCreationOptions(val type: EventCreationType, val name: String) - const val TEST_ADD_EVENT_BUTTON = "TEST_ADD_EVENT_BUTTON" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt index 588917e6ad..335e311f7f 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt @@ -44,7 +44,11 @@ fun TimelineEventsHeader( ) } if (timelineEventsHeaderModel.displayEventCreationButton) { - NewEventOptions(timelineEventsHeaderModel.options, onOptionSelected) + NewEventOptions( + options = timelineEventsHeaderModel.options, + addButtonTestTag = TEST_ADD_EVENT_BUTTON_IN_TIMELINE, + onOptionSelected = onOptionSelected, + ) } } } @@ -57,3 +61,5 @@ private fun TimelineEventHeaderPreview() { onOptionSelected = {}, ) } + +const val TEST_ADD_EVENT_BUTTON_IN_TIMELINE = "TEST_ADD_EVENT_BUTTON_IN_TIMELINE" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt index a12dc210a1..296e06979a 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import org.dhis2.R -import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.state.RelationshipTopBarIconState import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle import org.hisp.dhis.mobile.ui.designsystem.component.IconButton 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 a4c3375397..f4f2609029 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 @@ -265,4 +265,5 @@ class TeiDashboardCardMapper( this.filter { it.first.valueType() != ValueType.IMAGE } .filter { it.first.valueType() != ValueType.COORDINATE } .filter { it.first.valueType() != ValueType.FILE_RESOURCE } + .filter { it.second.value()?.isNotEmpty() == true } } diff --git a/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt b/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt index a411dda152..370bf310ff 100644 --- a/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt @@ -86,7 +86,7 @@ class TrackedEntityInstanceInfoProvider( ValueType.IMAGE, ValueType.FILE_RESOURCE, ValueType.COORDINATE, - ).contains(attribute.valueType) + ).contains(attribute.valueType) && attribute.value != null }.map { attribute -> AdditionalInfoItem( key = attribute.displayFormName, diff --git a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingFragment.kt b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingFragment.kt index 43e0405df3..ccadef0b34 100644 --- a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingFragment.kt @@ -11,11 +11,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.viewModels -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.main.MainActivity import org.dhis2.usescases.main.MainNavigator import org.dhis2.usescases.troubleshooting.ui.TroubleshootingScreen +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import javax.inject.Inject const val OPEN_LANGUAGE_SECTION = "OPEN_LANGUAGE_SECTION" @@ -62,7 +62,7 @@ class TroubleshootingFragment : FragmentGlobalAbstract() { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, ) - MdcTheme { + DHIS2Theme { TroubleshootingScreen(troubleshootingViewModel) { refreshScreenLanguageChange() } diff --git a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingViewModelFactory.kt index 3d47857992..cd355863c1 100644 --- a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingViewModelFactory.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.resources.LocaleSelector -@Suppress("UNCHECKED_CAST") class TroubleshootingViewModelFactory( private val localeSelector: LocaleSelector, private val troubleshootingRepository: TroubleshootingRepository, diff --git a/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUi.kt b/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUi.kt index b7d6118570..35b6cc272f 100644 --- a/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUi.kt +++ b/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUi.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -58,6 +57,8 @@ import org.dhis2.ui.MetadataIconData import org.dhis2.usescases.development.ProgramRuleValidation import org.dhis2.usescases.development.RuleValidation import org.dhis2.usescases.troubleshooting.TroubleshootingViewModel +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType import java.util.Locale @ExperimentalFoundationApi @@ -340,7 +341,7 @@ fun LoadingContent(loadingDescription: String) { .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - CircularProgressIndicator() + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) Spacer(modifier = Modifier.size(16.dp)) Text( text = loadingDescription, diff --git a/app/src/main/java/org/dhis2/utils/DateUtils.java b/app/src/main/java/org/dhis2/utils/DateUtils.java deleted file mode 100644 index 0a474a8dc5..0000000000 --- a/app/src/main/java/org/dhis2/utils/DateUtils.java +++ /dev/null @@ -1,923 +0,0 @@ -package org.dhis2.utils; - -import android.widget.DatePicker; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.dhis2.commons.date.Period; -import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker; -import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener; -import org.dhis2.commons.filters.FilterManager; -import org.dhis2.usescases.datasets.datasetInitial.DateRangeInputPeriodModel; -import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.utils.customviews.RxDateDialog; -import org.hisp.dhis.android.core.dataset.DataInputPeriod; -import org.hisp.dhis.android.core.event.EventStatus; -import org.hisp.dhis.android.core.period.DatePeriod; -import org.hisp.dhis.android.core.period.PeriodType; -import org.jetbrains.annotations.NotNull; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.reactivex.disposables.Disposable; -import timber.log.Timber; - -/** - * @deprecated This class is deprecated because it has been replaced by {@link org.dhis2.commons.date.DateUtils}. - * Please use {@link org.dhis2.commons.date.DateUtils} instead. - */ -@Deprecated(since = "2.10", forRemoval = true) -public class DateUtils { - - private static DateUtils instance; - private Calendar currentDateCalendar; - - public static DateUtils getInstance() { - if (instance == null) - instance = new DateUtils(); - - return instance; - } - - public static final String DATABASE_FORMAT_EXPRESSION_MILLIS = "yyyy-MM-dd'T'HH:mm:ss.SSS"; - public static final String DATABASE_FORMAT_EXPRESSION = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; - public static final String DATABASE_FORMAT_EXPRESSION_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ss"; - public static final String DATABASE_FORMAT_EXPRESSION_NO_SECONDS = "yyyy-MM-dd'T'HH:mm"; - public static final String DATE_TIME_FORMAT_EXPRESSION = "yyyy-MM-dd HH:mm"; - public static final String DATE_FORMAT_EXPRESSION = "yyyy-MM-dd"; - public static final String WEEKLY_FORMAT_EXPRESSION = "w yyyy"; - public static final String MONTHLY_FORMAT_EXPRESSION = "MMM yyyy"; - public static final String YEARLY_FORMAT_EXPRESSION = "yyyy"; - public static final String SIMPLE_DATE_FORMAT = "d/M/yyyy"; - public static final String TIME_24H_EXPRESSION = "HH:mm"; - - public Date[] getDateFromDateAndPeriod(Date date, Period period) { - switch (period) { - case YEARLY: - return new Date[]{getFirstDayOfYear(date), getLastDayOfYear(date)}; - case MONTHLY: - return new Date[]{getFirstDayOfMonth(date), getLastDayOfMonth(date)}; - case WEEKLY: - return new Date[]{getFirstDayOfWeek(date), getLastDayOfWeek(date)}; - case DAILY: - default: - return new Date[]{getDate(date), getDate(date)}; - } - } - - /********************** - CURRENT PEDIOD REGION*/ - - public Date getToday() { - return getCalendar().getTime(); - } - - /********************** - SELECTED PEDIOD REGION*/ - - private Date getDate(Date date) { - Calendar calendar = getCalendar(); - - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - return calendar.getTime(); - } - - private Date getNextDate(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.add(Calendar.DAY_OF_MONTH, 1); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - return calendar.getTime(); - } - - - private Date getFirstDayOfWeek(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); - - return calendar.getTime(); - } - - private Date getLastDayOfWeek(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); - calendar.add(Calendar.WEEK_OF_YEAR, 1); //Move to next week - calendar.add(Calendar.DAY_OF_MONTH, -1);//Substract one day to get last day of current week - - return calendar.getTime(); - } - - private Date getFirstDayOfMonth(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - calendar.set(Calendar.DAY_OF_MONTH, 1); - return calendar.getTime(); - } - - private Date getLastDayOfMonth(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.add(Calendar.MONTH, 1); - calendar.add(Calendar.DAY_OF_MONTH, -1); - return calendar.getTime(); - } - - private Date getFirstDayOfYear(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - calendar.set(Calendar.DAY_OF_YEAR, 1); - return calendar.getTime(); - } - - private Date getLastDayOfYear(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - calendar.set(Calendar.DAY_OF_YEAR, 1); - calendar.add(Calendar.YEAR, 1); - calendar.add(Calendar.DAY_OF_MONTH, -1); - return calendar.getTime(); - } - - @NonNull - public static SimpleDateFormat uiDateFormat() { - return new SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.US); - } - - @NonNull - public static SimpleDateFormat oldUiDateFormat() { - return new SimpleDateFormat(DATE_FORMAT_EXPRESSION, Locale.US); - } - - @NonNull - public static SimpleDateFormat timeFormat() { - return new SimpleDateFormat(TIME_24H_EXPRESSION, Locale.US); - } - - @NonNull - public static SimpleDateFormat dateTimeFormat() { - return new SimpleDateFormat(DATE_TIME_FORMAT_EXPRESSION, Locale.US); - } - - @NonNull - public static SimpleDateFormat databaseDateFormatMillis() { - return new SimpleDateFormat(DATABASE_FORMAT_EXPRESSION_MILLIS, Locale.US); - } - - @NonNull - public static SimpleDateFormat databaseDateFormat() { - return new SimpleDateFormat(DATABASE_FORMAT_EXPRESSION, Locale.US); - } - - @NonNull - public static SimpleDateFormat databaseDateFormatNoMillis() { - return new SimpleDateFormat(DATABASE_FORMAT_EXPRESSION_NO_MILLIS, Locale.US); - } - - @NonNull - public static SimpleDateFormat databaseDateFormatNoSeconds() { - return new SimpleDateFormat(DATABASE_FORMAT_EXPRESSION_NO_SECONDS, Locale.US); - } - - @NonNull - public static Boolean dateHasNoSeconds(String dateTime) { - try { - databaseDateFormatNoSeconds().parse(dateTime); - return true; - } catch (ParseException e) { - return false; - } - } - - - /********************** - FORMAT REGION*/ - public String formatDate(Date dateToFormat) { - return uiDateFormat().format(dateToFormat); - } - - public Calendar getCalendar() { - if (currentDateCalendar != null) - return currentDateCalendar; - - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - - public void setCurrentDate(Date date) { - currentDateCalendar = getCalendar(); - currentDateCalendar.setTime(date); - currentDateCalendar.set(Calendar.HOUR_OF_DAY, 0); - currentDateCalendar.set(Calendar.MINUTE, 0); - currentDateCalendar.set(Calendar.SECOND, 0); - currentDateCalendar.set(Calendar.MILLISECOND, 0); - } - - /********************** - COMPARE DATES REGION*/ - - public static int[] getDifference(Date startDate, Date endDate) { - org.joda.time.Period interval = new org.joda.time.Period(startDate.getTime(), endDate.getTime(), org.joda.time.PeriodType.yearMonthDayTime()); - return new int[]{interval.getYears(), interval.getMonths(), interval.getDays()}; - } - - /** - * @param currentDate Date from which calculation will be carried out. Default value is today. - * @param expiryDays Number of extra days to add events on previous period - * @param expiryPeriodType Expiry Period - * @return Min date to select - */ - public Date expDate(@Nullable Date currentDate, int expiryDays, @Nullable PeriodType expiryPeriodType) { - - Calendar calendar = getCalendar(); - - if (currentDate != null) - calendar.setTime(currentDate); - - Date date = calendar.getTime(); - - if (expiryPeriodType == null) { - return null; - } else { - switch (expiryPeriodType) { - case Daily: - calendar.add(Calendar.DAY_OF_YEAR, -expiryDays); - return calendar.getTime(); - case Weekly: - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - Date firstDateOfWeek = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfWeek.getTime()) >= expiryDays) { - return firstDateOfWeek; - } else { - calendar.add(Calendar.WEEK_OF_YEAR, -1); - } - break; - case WeeklyWednesday: - calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY); //moves to current week wednesday - Date wednesday = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - wednesday.getTime()) >= expiryDays) - return wednesday; - else - calendar.add(Calendar.WEEK_OF_YEAR, -1); - break; - case WeeklyThursday: - calendar.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY); //moves to current week wednesday - Date thursday = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - thursday.getTime()) >= expiryDays) - return thursday; - else - calendar.add(Calendar.WEEK_OF_YEAR, -1); - break; - case WeeklySaturday: - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); //moves to current week wednesday - Date saturday = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - saturday.getTime()) >= expiryDays) - return saturday; - else - calendar.add(Calendar.WEEK_OF_YEAR, -1); - break; - case WeeklySunday: - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); //moves to current week wednesday - Date sunday = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - sunday.getTime()) >= expiryDays) - return sunday; - else - calendar.add(Calendar.WEEK_OF_YEAR, -1); - break; - case BiWeekly: - if (calendar.get(Calendar.WEEK_OF_YEAR) % 2 == 0) //if true, we are in the 2nd week of the period - calendar.add(Calendar.WEEK_OF_YEAR, -1);//Moved to first week - calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); - Date firstDateOfBiWeek = calendar.getTime(); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfBiWeek.getTime()) >= expiryDays) { - return firstDateOfBiWeek; - } else { - calendar.add(Calendar.WEEK_OF_YEAR, -2); - } - break; - case Monthly: - Date firstDateOfMonth = getFirstDayOfMonth(calendar.getTime()); - calendar.setTime(firstDateOfMonth); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfMonth.getTime()) >= expiryDays) { - return firstDateOfMonth; - } else { - calendar.add(Calendar.MONTH, -1); - } - break; - case BiMonthly: - if (calendar.get(Calendar.MONTH) % 2 != 0) //January is 0, December is 11 - calendar.add(Calendar.MONTH, -1); //Moved to first month - Date firstDateOfBiMonth = getFirstDayOfMonth(calendar.getTime()); - calendar.setTime(firstDateOfBiMonth); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfBiMonth.getTime()) >= expiryDays) { - return firstDateOfBiMonth; - } else { - calendar.add(Calendar.MONTH, -2); - } - break; - case Quarterly: - while (calendar.get(Calendar.MONTH) % 4 != 0) //January is 0, December is 11 - calendar.add(Calendar.MONTH, -1); //Moved to first month - Date firstDateOfQMonth = getFirstDayOfMonth(calendar.getTime()); - calendar.setTime(firstDateOfQMonth); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfQMonth.getTime()) >= expiryDays) { - return firstDateOfQMonth; - } else { - calendar.add(Calendar.MONTH, -4); - } - break; - case SixMonthly: - while (calendar.get(Calendar.MONTH) % 6 != 0) //January is 0, December is 11 - calendar.add(Calendar.MONTH, -1); //Moved to first month - Date firstDateOfSixMonth = getFirstDayOfMonth(calendar.getTime()); - calendar.setTime(firstDateOfSixMonth); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfSixMonth.getTime()) >= expiryDays) { - return firstDateOfSixMonth; - } else { - calendar.add(Calendar.MONTH, -6); - } - break; - case SixMonthlyApril: - while ((calendar.get(Calendar.MONTH) - 3) % 6 != 0) //April is 0, December is 8 - calendar.add(Calendar.MONTH, -1); //Moved to first month - Date firstDateOfSixMonthApril = getFirstDayOfMonth(calendar.getTime()); - calendar.setTime(firstDateOfSixMonthApril); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfSixMonthApril.getTime()) >= expiryDays) { - return firstDateOfSixMonthApril; - } else { - calendar.add(Calendar.MONTH, -6); - } - break; - case Yearly: - Date firstDateOfYear = getFirstDayOfYear(calendar.getTime()); - calendar.setTime(firstDateOfYear); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfYear.getTime()) >= expiryDays) { - return firstDateOfYear; - } else { - calendar.add(Calendar.YEAR, -1); - } - break; - case FinancialApril: - calendar.set(Calendar.MONTH, Calendar.APRIL);//Moved to April - Date firstDateOfAprilYear = getFirstDayOfMonth(calendar.getTime()); //first day of April - calendar.setTime(firstDateOfAprilYear); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfAprilYear.getTime()) >= expiryDays) { - return firstDateOfAprilYear; - } else { - calendar.add(Calendar.YEAR, -1); //Moved to April last year - } - break; - case FinancialJuly: - calendar.set(Calendar.MONTH, Calendar.JULY);//Moved to July - Date firstDateOfJulyYear = getFirstDayOfMonth(calendar.getTime()); //first day of July - calendar.setTime(firstDateOfJulyYear); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfJulyYear.getTime()) >= expiryDays) { - return firstDateOfJulyYear; - } else { - calendar.add(Calendar.YEAR, -1); //Moved to July last year - } - break; - case FinancialOct: - calendar.set(Calendar.MONTH, Calendar.OCTOBER);//Moved to October - Date firstDateOfOctYear = getFirstDayOfMonth(calendar.getTime()); //first day of October - calendar.setTime(firstDateOfOctYear); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfOctYear.getTime()) >= expiryDays) { - return firstDateOfOctYear; - } else { - calendar.add(Calendar.YEAR, -1); //Moved to October last year - } - break; - } - - return calendar.getTime(); - } - } - - /** - * @param period Period in which the date will be selected - * @param currentDate Current selected date - * @param page 1 for next, 0 for now, -1 for previous - * @return Next/Previous date calculated from the currentDate and Period - */ - public Date getNextPeriod(PeriodType period, Date currentDate, int page) { - - Calendar calendar = Calendar.getInstance(); - calendar.setTime(currentDate); - int extra; - if (period == null) - period = PeriodType.Daily; - - switch (period) { - case Daily: - calendar.add(Calendar.DAY_OF_YEAR, page); - break; - case Weekly: - calendar.add(Calendar.WEEK_OF_YEAR, page); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - break; - case WeeklyWednesday: - calendar.setFirstDayOfWeek(Calendar.WEDNESDAY); - calendar.add(Calendar.WEEK_OF_YEAR, page); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY); - break; - case WeeklyThursday: - calendar.setFirstDayOfWeek(Calendar.THURSDAY); - calendar.add(Calendar.WEEK_OF_YEAR, page); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY); - break; - case WeeklySaturday: - calendar.setFirstDayOfWeek(Calendar.SATURDAY); - calendar.add(Calendar.WEEK_OF_YEAR, page); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); - break; - case WeeklySunday: - calendar.setFirstDayOfWeek(Calendar.SUNDAY); - calendar.add(Calendar.WEEK_OF_YEAR, page); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); - break; - case BiWeekly: - extra = calendar.get(Calendar.WEEK_OF_YEAR) % 2 == 0 ? 1 : 2; - calendar.add(Calendar.WEEK_OF_YEAR, page * extra); - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - break; - case Monthly: - calendar.add(Calendar.MONTH, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case BiMonthly: - extra = (calendar.get(Calendar.MONTH) + 1) % 2 == 0 ? 1 : 2; - calendar.add(Calendar.MONTH, page * extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case Quarterly: - extra = 3 * page - (calendar.get(Calendar.MONTH) % 3); - calendar.add(Calendar.MONTH, extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case SixMonthly: - extra = 6 * page - (calendar.get(Calendar.MONTH) % 6); - calendar.add(Calendar.MONTH, extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case SixMonthlyApril: - if (calendar.get(Calendar.MONTH) < Calendar.APRIL) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - } else if (calendar.get(Calendar.MONTH) >= Calendar.APRIL && calendar.get(Calendar.MONTH) < Calendar.OCTOBER) - calendar.set(Calendar.MONTH, Calendar.APRIL); - else - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.add(Calendar.MONTH, page * 6); - break; - case SixMonthlyNov: - if (calendar.get(Calendar.MONTH) < Calendar.MAY) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.NOVEMBER); - } else if (calendar.get(Calendar.MONTH) >= Calendar.MAY && calendar.get(Calendar.MONTH) < Calendar.NOVEMBER) - calendar.set(Calendar.MONTH, Calendar.MAY); - else - calendar.set(Calendar.MONTH, Calendar.NOVEMBER); - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.add(Calendar.MONTH, page * 6); - break; - case Yearly: - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_YEAR, 1); - break; - case FinancialApril: - if (calendar.get(Calendar.MONTH) < Calendar.APRIL) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.APRIL); - } else - calendar.set(Calendar.MONTH, Calendar.APRIL); - - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case FinancialJuly: - if (calendar.get(Calendar.MONTH) < Calendar.JULY) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.JULY); - } else - calendar.set(Calendar.MONTH, Calendar.JULY); - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case FinancialOct: - if (calendar.get(Calendar.MONTH) < Calendar.OCTOBER) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - } else - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case FinancialNov: - if (calendar.get(Calendar.MONTH) < Calendar.NOVEMBER) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.NOVEMBER); - } else - calendar.set(Calendar.MONTH, Calendar.NOVEMBER); - - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - default: - break; - } - return calendar.getTime(); - } - - /** - * @param period Period in which the date will be selected - * @param currentDate Current selected date - * @param page 1 for next, 0 for now, -1 for previous - * @return Next/Previous date calculated from the currentDate and Period - */ - public Date getNextPeriod(PeriodType period, Date currentDate, int page, boolean lastDate) { - - Calendar calendar = Calendar.getInstance(); - calendar.setTime(currentDate); - int extra; - if (period == null) - period = PeriodType.Daily; - - switch (period) { - case Daily: - calendar.add(Calendar.DAY_OF_YEAR, page); - break; - case Weekly: - calendar.add(Calendar.WEEK_OF_YEAR, page); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); - break; - case WeeklyWednesday: - calendar.add(Calendar.WEEK_OF_YEAR, page); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY); - break; - case WeeklyThursday: - calendar.add(Calendar.WEEK_OF_YEAR, page); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - break; - case WeeklySaturday: - calendar.add(Calendar.WEEK_OF_YEAR, page); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY); - break; - case WeeklySunday: - calendar.add(Calendar.WEEK_OF_YEAR, page); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); - break; - case BiWeekly: - extra = calendar.get(Calendar.WEEK_OF_YEAR) % 2 == 0 ? 1 : 2; - calendar.add(Calendar.WEEK_OF_YEAR, page * extra); - if (!lastDate) - calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); - else - calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); - break; - case Monthly: - calendar.add(Calendar.MONTH, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case BiMonthly: - extra = (calendar.get(Calendar.MONTH) + 1) % 2 == 0 ? 1 : 2; - calendar.add(Calendar.MONTH, page * extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case Quarterly: - extra = 1 + 4 - (calendar.get(Calendar.MONTH) + 1) % 4; - calendar.add(Calendar.MONTH, page * extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case SixMonthly: - extra = 1 + 6 - (calendar.get(Calendar.MONTH) + 1) % 6; - calendar.add(Calendar.MONTH, page * extra); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case SixMonthlyApril: - if (calendar.get(Calendar.MONTH) < Calendar.APRIL) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - } else if (calendar.get(Calendar.MONTH) >= Calendar.APRIL && calendar.get(Calendar.MONTH) < Calendar.OCTOBER) - calendar.set(Calendar.MONTH, Calendar.APRIL); - else - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.add(Calendar.MONTH, page * 6); - break; - case Yearly: - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_YEAR, 1); - break; - case FinancialApril: - if (calendar.get(Calendar.MONTH) < Calendar.APRIL) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.APRIL); - } else - calendar.set(Calendar.MONTH, Calendar.APRIL); - - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case FinancialJuly: - if (calendar.get(Calendar.MONTH) < Calendar.JULY) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.JULY); - } else - calendar.set(Calendar.MONTH, Calendar.JULY); - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - case FinancialOct: - if (calendar.get(Calendar.MONTH) < Calendar.OCTOBER) { - calendar.add(Calendar.YEAR, -1); - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - } else - calendar.set(Calendar.MONTH, Calendar.OCTOBER); - - calendar.add(Calendar.YEAR, page); - calendar.set(Calendar.DAY_OF_MONTH, 1); - break; - default: - break; - } - return calendar.getTime(); - } - - private int weekOfTheYear(PeriodType periodType, String periodId) { - Pattern pattern = Pattern.compile(periodType.getPattern()); - Matcher matcher = pattern.matcher(periodId); - int weekNumber = 0; - if (matcher.find()) { - weekNumber = Integer.parseInt(matcher.group(2)); - } - return weekNumber; - } - - /** - * Check if an event is expired in a date - * - * @param currentDate date or today if null - * @param completedDay date that event was completed - * @param compExpDays days of expiration of an event - * @return true or false - */ - public Boolean isEventExpired(@Nullable Date currentDate, Date completedDay, int compExpDays) { - - Calendar calendar = getCalendar(); - - if (currentDate != null) - calendar.setTime(currentDate); - - Date date = calendar.getTime(); - - return completedDay != null && compExpDays > 0 && - completedDay.getTime() + TimeUnit.DAYS.toMillis(compExpDays) < date.getTime(); - } - - /** - * Check if an event is expired today. - * - * @param eventDate Date of the event (Can be either eventDate or dueDate, but can not be null). - * @param completeDate date that event was completed (can be null). - * @param status status of event (ACTIVE,COMPLETED,SCHEDULE,OVERDUE,SKIPPED,VISITED). - * @param compExpDays extra days to edit event when completed . - * @param programPeriodType period in which the event can be edited. - * @param expDays extra days after period to edit event. - * @return true or false - */ - public Boolean isEventExpired(Date eventDate, Date completeDate, EventStatus status, int compExpDays, PeriodType programPeriodType, int expDays) { - if (status == EventStatus.COMPLETED && completeDate == null) - return false; - - boolean expiredBecouseOfPeriod; - boolean expiredBecouseOfCompletion = false; - - expiredBecouseOfCompletion = status == EventStatus.COMPLETED ? - isEventExpired(null, eventDate, compExpDays) : false; - - if (programPeriodType != null) { - Date expDate = getNextPeriod(programPeriodType, eventDate, 1); //Initial date of next period - Date currentDate = getCalendar().getTime(); - if (expDays > 0) { - Calendar calendar = getCalendar(); - calendar.setTime(expDate); - calendar.add(Calendar.DAY_OF_YEAR, expDays); - expDate = calendar.getTime(); - } - expiredBecouseOfPeriod = expDate != null && expDate.compareTo(currentDate) <= 0; - - return expiredBecouseOfPeriod || expiredBecouseOfCompletion; - } else - return expiredBecouseOfCompletion; - - } - - - public Boolean isDataSetExpired(int expiredDays, Date periodInitialDate) { - return Calendar.getInstance().getTime().getTime() > periodInitialDate.getTime() + TimeUnit.DAYS.toMillis(expiredDays); - } - - public Boolean isInsideInputPeriod(DataInputPeriod dataInputPeriodModel) { - if (dataInputPeriodModel.openingDate() == null && dataInputPeriodModel.closingDate() != null) - return Calendar.getInstance().getTime().getTime() < dataInputPeriodModel.closingDate().getTime(); - - if (dataInputPeriodModel.openingDate() != null && dataInputPeriodModel.closingDate() == null) - return dataInputPeriodModel.openingDate().getTime() < Calendar.getInstance().getTime().getTime(); - - if (dataInputPeriodModel.openingDate() == null && dataInputPeriodModel.closingDate() == null) - return true; - - return dataInputPeriodModel.openingDate().getTime() < Calendar.getInstance().getTime().getTime() - && Calendar.getInstance().getTime().getTime() < dataInputPeriodModel.closingDate().getTime(); - } - - public Boolean isInsideFutureInputPeriod(DateRangeInputPeriodModel inputPeriod, Integer futureOpenDays) { - if (futureOpenDays != null && futureOpenDays > 0) { - boolean isInside = false; - - Date today = DateUtils.getInstance().getToday(); - Date inputPeriodOpeningDate = inputPeriod.endPeriodDate(); - - long diffInMillis = Math.abs(inputPeriodOpeningDate.getTime() - today.getTime()); - long diffInDays = TimeUnit.DAYS.convert(diffInMillis, TimeUnit.MILLISECONDS); - - - if (diffInDays < futureOpenDays) { - isInside = true; - } - return isInside; - } else { - return false; - } - } - - public List getDatePeriodListFor(List selectedDates, Period period) { - List datePeriods = new ArrayList<>(); - for (Date date : selectedDates) { - Date[] startEndDates = getDateFromDateAndPeriod(date, period); - datePeriods.add(DatePeriod.builder().startDate(startEndDates[0]).endDate(startEndDates[1]).build()); - } - return datePeriods; - } - - public void fromCalendarSelector(ActivityGlobalAbstract activity, OnFromToSelector fromToListener) { - Date startDate = null; - if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) - startDate = FilterManager.getInstance().getPeriodFilters().get(0).startDate(); - - CalendarPicker dialog = new CalendarPicker(activity.getContext()); - dialog.setTitle(null); - dialog.setInitialDate(startDate); - dialog.isFutureDatesAllowed(true); - dialog.setListener(new OnDatePickerListener() { - @Override - public void onNegativeClick() { - } - - @Override - public void onPositiveClick(@NotNull DatePicker datePicker) { - toCalendarSelector(datePicker, activity, fromToListener); - } - }); - dialog.show(); - } - - private void toCalendarSelector(DatePicker datePicker, ActivityGlobalAbstract activity, OnFromToSelector fromToListener) { - Calendar fromDate = Calendar.getInstance(); - fromDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); - - Date endDate = null; - if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) - endDate = FilterManager.getInstance().getPeriodFilters().get(0).endDate(); - - CalendarPicker dialog = new CalendarPicker(activity.getContext()); - dialog.setTitle(null); - dialog.setInitialDate(endDate); - dialog.setMinDate(fromDate.getTime()); - dialog.isFutureDatesAllowed(true); - dialog.setListener(new OnDatePickerListener() { - @Override - public void onNegativeClick() { - } - - @Override - public void onPositiveClick(@NotNull DatePicker datePicker) { - Calendar toDate = Calendar.getInstance(); - toDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); - List dates = new ArrayList<>(); - dates.add(DatePeriod.builder().startDate(fromDate.getTime()).endDate(toDate.getTime()).build()); - fromToListener.onFromToSelected(dates); - } - }); - dialog.show(); - } - - public void showPeriodDialog(ActivityGlobalAbstract activity, OnFromToSelector fromToListener, boolean fromOtherPeriod) { - Date startDate = null; - if (!FilterManager.getInstance().getPeriodFilters().isEmpty()) - startDate = FilterManager.getInstance().getPeriodFilters().get(0).startDate(); - - CalendarPicker dialog = new CalendarPicker(activity.getContext()); - dialog.setTitle("Daily"); - dialog.setInitialDate(startDate); - dialog.isFutureDatesAllowed(true); - dialog.isFromOtherPeriods(fromOtherPeriod); - dialog.setListener(new OnDatePickerListener() { - @Override - public void onNegativeClick() { - Disposable disposable = new RxDateDialog(activity, Period.WEEKLY) - .createForFilter().show() - .subscribe( - selectedDates -> fromToListener.onFromToSelected(getDatePeriodListFor(selectedDates.val1(), - selectedDates.val0())), - Timber::e - ); - } - - @Override - public void onPositiveClick(@NotNull DatePicker datePicker) { - Calendar chosenDate = Calendar.getInstance(); - chosenDate.set(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth()); - List dates = new ArrayList<>(); - dates.add(chosenDate.getTime()); - fromToListener.onFromToSelected(getDatePeriodListFor(dates, Period.DAILY)); - } - }); - dialog.show(); - } - - public interface OnFromToSelector { - void onFromToSelected(List datePeriods); - } -} diff --git a/app/src/main/java/org/dhis2/utils/HelpManager.java b/app/src/main/java/org/dhis2/utils/HelpManager.java index 4c069b37e3..0d995029e9 100644 --- a/app/src/main/java/org/dhis2/utils/HelpManager.java +++ b/app/src/main/java/org/dhis2/utils/HelpManager.java @@ -25,8 +25,8 @@ public class HelpManager { private NestedScrollView scrollView; public enum TutorialName { - SETTINGS_FRAGMENT, PROGRAM_FRAGMENT, TEI_DASHBOARD, TEI_SEARCH, PROGRAM_EVENT_LIST, - EVENT_DETAIL, EVENT_SUMMARY, EVENT_INITIAL + SETTINGS_FRAGMENT, PROGRAM_FRAGMENT, TEI_DASHBOARD, + EVENT_INITIAL } public static HelpManager getInstance() { @@ -67,8 +67,6 @@ public void show(ActivityGlobalAbstract activity, TutorialName name, SparseBoole case PROGRAM_FRAGMENT -> help = programFragmentTutorial(activity, stepCondition); case SETTINGS_FRAGMENT -> help = settingsFragmentTutorial(activity); case TEI_DASHBOARD -> help = teiDashboardTutorial(activity); - case TEI_SEARCH -> help = teiSearchTutorial(activity); - case PROGRAM_EVENT_LIST -> help = programEventListTutorial(activity, stepCondition); case EVENT_INITIAL -> help = eventInitialTutorial(activity, stepCondition); } if (!help.isEmpty()) @@ -99,48 +97,6 @@ private List eventInitialTutorial(ActivityGlobalAbstract acti return steps; } - private List programEventListTutorial(ActivityGlobalAbstract activity, SparseBooleanArray stepCondition) { - ArrayList steps = new ArrayList<>(); - - FancyShowCaseView tuto1 = new FancyShowCaseView.Builder(activity) - .title(activity.getString(R.string.tuto_program_event_1)) - .enableAutoTextPosition() - .closeOnTouch(true) - .build(); - steps.add(tuto1); - - if (stepCondition.get(2)) { - FancyShowCaseView tuto2 = new FancyShowCaseView.Builder(activity) - .title(activity.getString(R.string.tuto_program_event_2)) - .enableAutoTextPosition() - .focusOn(activity.findViewById(R.id.addEventButton)) - .closeOnTouch(true) - .build(); - steps.add(tuto2); - } - return steps; - } - - private List teiSearchTutorial(ActivityGlobalAbstract activity) { - FancyShowCaseView tuto1 = new FancyShowCaseView.Builder(activity) - .title(activity.getString(R.string.tuto_search_1_v2)) - .enableAutoTextPosition() - .closeOnTouch(true) - .build(); - FancyShowCaseView tuto2 = new FancyShowCaseView.Builder(activity) - .title(activity.getString(R.string.tuto_search_2)) - .enableAutoTextPosition() - .focusShape(FocusShape.ROUNDED_RECTANGLE) - .focusOn(activity.findViewById(R.id.program_spinner)) - .closeOnTouch(true) - .build(); - - ArrayList steps = new ArrayList<>(); - steps.add(tuto1); - steps.add(tuto2); - return steps; - } - private List teiDashboardTutorial(ActivityGlobalAbstract activity) { FancyShowCaseView tuto2 = null; FancyShowCaseView tuto1 = new FancyShowCaseView.Builder(activity) diff --git a/app/src/main/java/org/dhis2/utils/customviews/DateAdapter.java b/app/src/main/java/org/dhis2/utils/customviews/DateAdapter.java index 6c375a4f75..d6c02a6314 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/DateAdapter.java +++ b/app/src/main/java/org/dhis2/utils/customviews/DateAdapter.java @@ -9,8 +9,8 @@ import org.dhis2.R; import org.dhis2.commons.data.tuples.Pair; +import org.dhis2.commons.date.DateUtils; import org.dhis2.databinding.ItemDateBinding; -import org.dhis2.utils.DateUtils; import org.dhis2.commons.date.Period; import java.text.SimpleDateFormat; @@ -29,15 +29,14 @@ public class DateAdapter extends RecyclerView.Adapter { private Period currentPeriod = null; - private List datesNames = new ArrayList<>(); - private List seletedDatesName = new ArrayList<>(); - private List dates = new ArrayList<>(); + private final List datesNames = new ArrayList<>(); + private final List seletedDatesName = new ArrayList<>(); + private final List dates = new ArrayList<>(); private Pair> selectedDates; - private SimpleDateFormat dayFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()); - private SimpleDateFormat weeklyFormat = new SimpleDateFormat("'Week' w", Locale.getDefault()); - private String weeklyFormatWithDates = "%s, %s / %s"; - private SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM yyyy", Locale.getDefault()); - private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); + private final SimpleDateFormat dayFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()); + private final SimpleDateFormat weeklyFormat = new SimpleDateFormat("'Week' w", Locale.getDefault()); + private final SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM yyyy", Locale.getDefault()); + private final SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); private Map mapPeriod = new HashMap<>(); public DateAdapter(Period period) { @@ -56,6 +55,7 @@ private void initData(Period period) { do { String date = null; + String weeklyFormatWithDates = "%s, %s / %s"; switch (period) { case WEEKLY: date = weeklyFormat.format(calendar.getTime()); //Get current week @@ -113,14 +113,14 @@ public DateViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public void onBindViewHolder(DateViewHolder holder, int position) { holder.bind(datesNames.get(position)); - if ((dates.size() > 0 && selectedDates.val1().contains(dates.get(position))) || (datesNames.size() > 0 && seletedDatesName.contains(datesNames.get(position)))) { + if ((!dates.isEmpty() && selectedDates.val1().contains(dates.get(position))) || (!datesNames.isEmpty() && seletedDatesName.contains(datesNames.get(position)))) { holder.itemView.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.colorPrimaryLight)); } else { holder.itemView.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.white)); } holder.itemView.setOnClickListener(view -> { - if (mapPeriod == null || mapPeriod.size() == 0) { + if (mapPeriod == null || mapPeriod.isEmpty()) { if (!selectedDates.val1().contains(dates.get(position))) { selectedDates.val1().add(dates.get(position)); holder.itemView.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.colorPrimaryLight)); @@ -143,9 +143,9 @@ public void onBindViewHolder(DateViewHolder holder, int position) { @Override public int getItemCount() { - if (mapPeriod != null && mapPeriod.size() > 0) + if (mapPeriod != null && !mapPeriod.isEmpty()) return mapPeriod.size(); - return datesNames != null ? datesNames.size() : 0; + return datesNames.size(); } public Pair> clearFilters() { diff --git a/app/src/main/java/org/dhis2/utils/customviews/DateDialog.java b/app/src/main/java/org/dhis2/utils/customviews/DateDialog.java index 5d13875185..bbe6d26087 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/DateDialog.java +++ b/app/src/main/java/org/dhis2/utils/customviews/DateDialog.java @@ -1,6 +1,7 @@ package org.dhis2.utils.customviews; import android.app.Dialog; +import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -13,20 +14,23 @@ import androidx.fragment.app.DialogFragment; import org.dhis2.R; +import org.dhis2.commons.data.tuples.Pair; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.date.Period; import org.dhis2.commons.filters.FilterManager; -import org.dhis2.commons.data.tuples.Pair; import org.dhis2.databinding.DialogDateBinding; import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.utils.DateUtils; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import io.reactivex.Single; import io.reactivex.SingleEmitter; +import io.reactivex.disposables.CompositeDisposable; +import timber.log.Timber; /** * QUADRAM. Created by ppajuelo on 05/12/2017. @@ -45,6 +49,7 @@ public class DateDialog extends DialogFragment { private View.OnClickListener possitiveListener; private View.OnClickListener negativeListener; private static ActivityGlobalAbstract activity; + private CompositeDisposable disposable = new CompositeDisposable(); public static DateDialog newInstace(ActionTrigger mActionTrigger, Period mPeriod) { if (period != mPeriod || instace == null) { @@ -116,19 +121,35 @@ public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup c return binding.getRoot(); } - private void manageButtonPeriods(boolean next){ + private void manageButtonPeriods(boolean next) { Period period = adapter.swapPeriod(next); - if(period == Period.DAILY) { - DateUtils.getInstance().showPeriodDialog(activity, datePeriods -> - FilterManager.getInstance().addPeriod( - null - ), true); + if (period == Period.DAILY) { + DateUtils.OnFromToSelector fromToSelector = datePeriods -> + FilterManager.getInstance().addPeriod(null); + DateUtils.getInstance().showPeriodDialog( + activity, + fromToSelector, + true, + () -> disposable.add(new RxDateDialog((ActivityGlobalAbstract) requireActivity(), Period.WEEKLY) + .createForFilter().show() + .subscribe( + periods -> fromToSelector.onFromToSelected(new ArrayList<>()), + Timber::d + ) + ) + ); dismiss(); } binding.setTitleText(getString(period.getNameResouce())); } + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + disposable.clear(); + } + private DateDialog withEmitter(final SingleEmitter>> emitter) { this.callback = emitter; return this; diff --git a/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt b/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt index b01a0607f2..dbcc3cd84f 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt +++ b/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt @@ -1,13 +1,18 @@ package org.dhis2.utils.customviews +import android.content.res.Resources +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset import org.hisp.dhis.mobile.ui.designsystem.component.IconButton import org.hisp.dhis.mobile.ui.designsystem.component.menu.DropDownMenu import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable @@ -29,13 +34,20 @@ fun MoreOptionsWithDropDownMenuButton( ) { onMenuToggle(!expanded) } - DropDownMenu( + modifier = Modifier.widthIn(max = 0.7.dw), items = dropDownMenuItems, expanded = expanded, + offset = DpOffset(x = -Spacing.Spacing16, y = Spacing.Spacing0), onDismissRequest = { onMenuToggle(false) }, - ) { itemId -> - onMenuToggle(false) - onItemClick(itemId) - } + onItemClick = { itemId -> + onMenuToggle(false) + onItemClick(itemId) + }, + ) } + +inline val Double.dw: Dp + get() = Resources.getSystem().displayMetrics.let { + Dp(value = ((this * it.widthPixels) / it.density).toFloat()) + } diff --git a/app/src/main/java/org/dhis2/utils/customviews/PeriodAdapter.kt b/app/src/main/java/org/dhis2/utils/customviews/PeriodAdapter.kt index 01a9dbc18c..544f216a02 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/PeriodAdapter.kt +++ b/app/src/main/java/org/dhis2/utils/customviews/PeriodAdapter.kt @@ -5,10 +5,10 @@ import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView import org.dhis2.R +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.databinding.ItemDateBinding import org.dhis2.usescases.datasets.datasetInitial.DateRangeInputPeriodModel -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.period.PeriodType import java.util.Date @@ -28,12 +28,11 @@ private class PeriodAdapter( const val DEFAULT_PERIODS_SIZE = 10 } - private val datePeriods: MutableList + private val datePeriods: MutableList = ArrayList() private var lastDate: Date private var currentDate = DateUtils.getInstance().today init { - datePeriods = ArrayList() lastDate = DateUtils.getInstance().getNextPeriod( periodType, organisationUnit?.closedDate() ?: DateUtils.getInstance().today, @@ -73,7 +72,7 @@ private class PeriodAdapter( ), ) holder.itemView.setOnClickListener { - listener(datePeriods[holder.adapterPosition]) + listener(datePeriods[holder.bindingAdapterPosition]) } } } @@ -136,7 +135,7 @@ private class PeriodAdapter( return if (isFuturePeriodsConfigured && isPeriodInTheFuture) { val isInsideInitialPeriodDate = DateUtils.getInstance() - .isInsideFutureInputPeriod(inputPeriodModel, openFuturePeriods) + .isInsideFutureInputPeriod(inputPeriodModel.endPeriodDate(), openFuturePeriods) isInsideInitialPeriodDate && isInsideOpenDates && isInsideCloseDates } else { diff --git a/app/src/main/java/org/dhis2/utils/customviews/PeriodDialogInputPeriod.java b/app/src/main/java/org/dhis2/utils/customviews/PeriodDialogInputPeriod.java index 00749b359b..a8e9a223a5 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/PeriodDialogInputPeriod.java +++ b/app/src/main/java/org/dhis2/utils/customviews/PeriodDialogInputPeriod.java @@ -10,10 +10,10 @@ import androidx.databinding.DataBindingUtil; import org.dhis2.R; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.dialogs.PeriodDialog; import org.dhis2.databinding.DialogPeriodDatesBinding; import org.dhis2.usescases.datasets.datasetInitial.DateRangeInputPeriodModel; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import java.util.ArrayList; @@ -84,12 +84,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (!isAllowed && !isEmpty) { binding.noPeriods.setText(getString(R.string.there_is_no_available_period)); }else{ - boolean withInputPeriod = false; - - if(isAllowed && !isEmpty){ - withInputPeriod = true; - } - PeriodAdapter periodAdapter = new PeriodAdapter( getPeriod(), openFuturePeriods != null ? openFuturePeriods : 0, @@ -97,7 +91,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c getPossitiveListener().onDateSet(date); return Unit.INSTANCE; }, - withInputPeriod, + isAllowed && !isEmpty, organisationUnit, inputPeriod, periodUtils); diff --git a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt index 61ae035dd1..3fba0c8dee 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt @@ -82,7 +82,7 @@ class GranularSyncRepository( ConflictType.TEI -> { val enrollment = d2.enrollment(syncContext.recordUid()) - d2.observeTei(enrollment?.trackedEntityInstance()!!) + d2.observeTei(enrollment?.trackedEntityInstance() ?: syncContext.recordUid()) .map { it.aggregatedSyncState() } } @@ -121,7 +121,7 @@ class GranularSyncRepository( ConflictType.TEI -> { val enrollment = d2.enrollment(syncContext.recordUid()) - d2.observeTei(enrollment?.trackedEntityInstance()!!) + d2.observeTei(enrollment?.trackedEntityInstance() ?: syncContext.recordUid()) .map { SyncDate(it.lastUpdated()) } } @@ -966,7 +966,7 @@ class GranularSyncRepository( d2.enrollmentModule().enrollments().uid(syncContext.recordUid()).blockingGet() d2.trackedEntityModule().trackedEntityTypes().uid( d2.trackedEntityModule().trackedEntityInstances() - .uid(enrollment?.trackedEntityInstance()!!) + .uid(enrollment?.trackedEntityInstance() ?: syncContext.recordUid()) .blockingGet()?.trackedEntityType(), ) .get().map { it.displayName() } diff --git a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncViewModelFactory.kt b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncViewModelFactory.kt index b9f88c6e11..489b3ac926 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncViewModelFactory.kt @@ -8,7 +8,6 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.workManager.WorkManagerController import org.hisp.dhis.android.core.D2 -@Suppress("UNCHECKED_CAST") class GranularSyncViewModelFactory( private val d2: D2, private val view: GranularSyncContracts.View, diff --git a/app/src/main/java/org/dhis2/utils/granularsync/SMSSenderHelper.kt b/app/src/main/java/org/dhis2/utils/granularsync/SMSSenderHelper.kt index a59b8e4f9e..317bc82715 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/SMSSenderHelper.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/SMSSenderHelper.kt @@ -82,7 +82,7 @@ class SMSSenderHelper( .show(fragmentManager, BottomSheetDialogUiModel::class.java.simpleName) } - private fun createSMSIntent(message: String, smsToNumber: String): Intent? { + private fun createSMSIntent(message: String, smsToNumber: String): Intent { val uri = Uri.parse("smsto:$smsToNumber") val intent = Intent(Intent.ACTION_SENDTO).apply { data = uri diff --git a/app/src/main/java/org/dhis2/utils/granularsync/SyncConflictHolder.kt b/app/src/main/java/org/dhis2/utils/granularsync/SyncConflictHolder.kt index 75c288fa12..01567e0b55 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/SyncConflictHolder.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/SyncConflictHolder.kt @@ -1,8 +1,8 @@ package org.dhis2.utils.granularsync import androidx.recyclerview.widget.RecyclerView +import org.dhis2.commons.date.DateUtils import org.dhis2.databinding.ItemSyncConflictBinding -import org.dhis2.utils.DateUtils class SyncConflictHolder(private val binding: ItemSyncConflictBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/org/dhis2/widgets/DhisCustomLauncher.kt b/app/src/main/java/org/dhis2/widgets/DhisCustomLauncher.kt index eaf911d5d5..5734680e3e 100644 --- a/app/src/main/java/org/dhis2/widgets/DhisCustomLauncher.kt +++ b/app/src/main/java/org/dhis2/widgets/DhisCustomLauncher.kt @@ -21,7 +21,7 @@ class DhisCustomLauncher : AppWidgetProvider() { val remoteViews = RemoteViews(context.packageName, R.layout.dhis_custom_launcher) val configIntent = Intent(context, SplashActivity::class.java) - val configPendingIntent = PendingIntent.getActivity(context, 0, configIntent, 0) + val configPendingIntent = PendingIntent.getActivity(context, 0, configIntent, PendingIntent.FLAG_IMMUTABLE) remoteViews.setOnClickPendingIntent(R.id.appwidget_image, configPendingIntent) appWidgetManager.updateAppWidget(appWidgetIds, remoteViews) @@ -80,7 +80,7 @@ class DhisCustomLauncher : AppWidgetProvider() { private fun getPendingIntent(context: Context): PendingIntent { val intent = Intent(context, SplashActivity::class.java) - return PendingIntent.getActivity(context, 0, intent, 0) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } } } diff --git a/app/src/main/res/layout/activity_dataset_initial.xml b/app/src/main/res/layout/activity_dataset_initial.xml index 2a7ff5198e..8a25ddb0ca 100644 --- a/app/src/main/res/layout/activity_dataset_initial.xml +++ b/app/src/main/res/layout/activity_dataset_initial.xml @@ -29,7 +29,6 @@ android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" android:background="?colorPrimary" - android:elevation="10dp" android:gravity="center_vertical" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -59,6 +58,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:padding="10dp" + android:background="@drawable/ic_front_home_backdrop_bg" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar"> diff --git a/app/src/main/res/layout/activity_event_scheduled.xml b/app/src/main/res/layout/activity_event_scheduled.xml index 1e1f5a014d..f13afb3662 100644 --- a/app/src/main/res/layout/activity_event_scheduled.xml +++ b/app/src/main/res/layout/activity_event_scheduled.xml @@ -60,7 +60,6 @@ android:text="@{ name }" android:textColor="@android:color/white" android:textSize="20sp" - app:layout_constraintEnd_toStartOf="@+id/moreOptions" app:layout_constraintStart_toEndOf="@id/menu" tools:text="TITLE\ntest\n234" /> @@ -68,6 +67,7 @@ + + + + + + + + + + + @@ -63,8 +63,8 @@ + app:layout_constraintTop_toBottomOf="@+id/toolbar_guideline" /> - - - - + app:layout_constraintTop_toBottomOf="@id/backdropGuideTop" /> diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6b24f15f0b..206de4c263 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -99,7 +99,6 @@ انهاء ومكتمل مكتمل تم الإلغاء - إعادة فتح تنشيط فعال @@ -458,7 +457,6 @@ إعتمادات فارغة لتتمكن من تسجيل الدخول باستخدام ماسح بصمة الإصبع، تحتاج أولاً إلى تسجيل الدخول بإستخدام الطريقة الافتراضية الأمن البيومتري - استخدام بصمة إصبعك لتسجيل الدخول مع هذا المستخدم؟ جداول الجدول https:// @@ -707,6 +705,13 @@ لم نتمكن من بدء الجلسة مع المستخدم الخاص بك هناك أحداث مخطط لها لن تتم إعادة جدولتها المخططات البيانية + عرض جدول + التحليلات + المهام + البرامج + الملاحظات + إدخال البيانات + التفاصيل عرض كشريط مشاهدة على شكل خطي عرض كجدول diff --git a/app/src/main/res/values-b+uz+Latn/strings.xml b/app/src/main/res/values-b+uz+Latn/strings.xml index 72817baa74..55644db37d 100644 --- a/app/src/main/res/values-b+uz+Latn/strings.xml +++ b/app/src/main/res/values-b+uz+Latn/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 DHIS @@ -84,7 +84,6 @@ Yakunlash va toʼldirish bajarildi Bekor qilindi - qayta ochish Faollashtirish Faol @@ -423,7 +422,6 @@ Ishonch yorliqlari boʼsh Barmoq izlari skaneri yordamida tizimga kirish uchun avval standart usuldan foydalanishingiz kerak Biometrik Xavfsizlik - Foydalanuvchi tizimga kirishi uchun barmoq izlaridan foydalanilsinmi? Jadvallar Жадвал https:// @@ -664,6 +662,13 @@ Xatto haqida maʼlumot Rejalashtirila olinmaydigan hodisa/tadbir rejalari mavjud Diagrammalar + Jadval koʼrinishi + Аnalitika + Vazifalar + Dasturlar + Izohlar + Maʼlumot kiritish + Тафсилотлари Jadval sifatida koʼrish Ушбу йил Ushbu juft oylik diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 544bf61637..02a42d3c6d 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -1,4 +1,4 @@ - + طةران نويَ كردنةوة diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7bc9dfb1ac..7ac7ec8444 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -97,7 +97,6 @@ Hotovo a dokončit Dokončeno Zrušeno - znovu otevřít Aktivovat Aktivní @@ -454,7 +453,6 @@ Prázdné pověření Abyste se mohli přihlásit pomocí snímače otisků prstů, musíte nejprve použít výchozí způsob Biometrické zabezpečení - Chcete se tímto uživatelem přihlásit pomocí otisku prstu? Tabulky Tabulka https:// @@ -704,6 +702,13 @@ Nemohli jsme zahájit relaci s vaším uživatelem Jsou plánované události, které nebudou znovu naplánovány Grafy + Zobrazení tabulky + Analýza + Úkoly + Programy + Poznámky + Vstup dat + Detaily Zobrazit jako lištu Zobrazit jako čáru Zobrazit jako tabulku diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 976b4bd3e2..e730451c43 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,4 +1,4 @@ - + DHIS 2 DHIS @@ -44,8 +44,8 @@ %s resultados Agregar nuevo Añadir - Introduce %s - Introduce / Salta %s + Ingresar %s + Editar / Omitir %s Programar Remitir @@ -78,7 +78,7 @@ Buscar - Buscar / Añadir nuevo %s + Buscar / Agregar %s Buscar %s Añadir nuevo %s Introducir texto @@ -104,6 +104,7 @@ Completado Cancelar Reabrir + Reabrir formulario Activar Activo @@ -143,15 +144,17 @@ No hay periodos disponibles. Fecha de vencimiento + Siguiente %1$s Cat Combo DD/MM/YYYY Cancelar Borrar Total Fecha de evento más reciente - No hay opciones disponibles - Listado de admisiones + + + Lista de inscripción Programas activos Programas disponibles para inscripción Admitir @@ -168,15 +171,17 @@ Desactivar Este programa no admite más eventos + No hay datos Eventos Inicio Información de la App Cerrar Sesion Bloquear sesion - Mostrar %s más... Mostrar menos... + + 1 minuto 15 minutos 30 minutos @@ -202,7 +207,7 @@ Modulo SMS: SMS gateway Emisor del SMS de resultado - "Tiempo de espera de recepción del SMS " + Tiempo de espera para SMS (seg) segundos numero numero @@ -235,12 +240,12 @@ Se borrará la configuración y todos los datos guardados por la App. Tendrá que iniciar sesión de nuevo y los datos que no se hayan enviado al servidor se perderán. Configuración SMS Verifique los parámetros relacionados con la SMS gateway - - Actualización de software Software Exportar base de datos - Exporta tu base de datos y compártala con su administrador + Exporta tu base de datos y compártela con tu administrador + + Fecha de inscripción @@ -383,11 +388,12 @@ Selecciona las fechas para filtrar Seleccionar opción Incidencia reportada + Generar nuevo evento ¿Quiere crear un nuevo evento? Eventos Completados Todos los eventos de este programa están completados. ¿Quiere completar el programa también? ¿ Completar también la inscripción? - Le gustaría completar el %s también? + ¿Le gustaría completar %s también? Ver Inicio @@ -472,7 +478,8 @@ Vaciar credenciales Para poder iniciar sesión por huella dactilar debe entrar primero por el modo normal Seguridad biométrica - Usar huella dactilar para acceder con este usuario? + Acceso rápido + ¿Te gustaría usar tu huella o algún otro método biométrico para acceder? Tablas Tabla https:// @@ -483,8 +490,8 @@ Debe seleccionar al menos una unidad organizativa. Enviar datos Ayude a DHIS2 a mejorar esta app compartiendo errores asociados a su usuario. - Quiere ayudarnos a mejorar esta app? - "Si dice que sí la app mandará estadísticas anónimas y registro de errores que ayudaran a mejorar la experiencia y el rendimiento.\n\nPara más detalles, consulte nuestra política de privacidad. " + ¿Desea ayudarnos a mejorar esta aplicación? + Si seleccionas sí, la aplicación enviará estadísticas anónimas y registros de errores que nos ayudarán a mejorar la experiencia del usuario y el rendimiento. Para obtener más detalles, consulta nuestra política de privacidad. política de privacidad Comprobar campos Se perdió la conexión a internet durante la sincronización. Inténtelo mas tarde. @@ -698,6 +705,7 @@ El %s seleccionado no puede ser descargado. Completado por %s: %s Actualizado: %s + Terminado No hemos podido verificar la calidad de los datos. Contacte con su administrador ¿Está seguro? ¿Quiere reabrir este set de datos? @@ -725,6 +733,18 @@ No pudimos iniciar la sesión con su usuario Hay eventos planificados que no serán reprogramados Gráficas + Mostrar + Vista de tabla + Mapa + Analíticas + Tareas + Programas + Gráficas + Relaciones + Notas + Introducción de datos + Formulario + Detalles Ver como barras Ver como líneas Ver como tabla @@ -827,6 +847,7 @@ De acuerdo Empieza una búsqueda para encontrar %s Puedes buscar o crear un nuevo %s + Tienes algunos mensajes de aviso. Tienes algunos mensajes de alerta.\n¿Desea marcar el formulario como Completo? Algunos campos tienen errores y no serán guardados. \n¿Desea revisar el formulario? ¿Desea marcar este formulario como completo? @@ -842,7 +863,7 @@ Dispositivo: %s Versión del SO: %s Abrir detalles de %s - Detalles de %s + %s detalles Sincronizando %s... SMS enviado. Esperando respuesta del servidor. Se ha confirmado que el SMS llegó al servidor. Actualizando estado @@ -854,82 +875,112 @@ ¿Ha enviado el sms? Esperando confirmación manual Sincronizando eventos - Sincronizando entidades de ratreo + Sincronizando entidades monitoreadas Sincronizando set de datos Sincronizando recursos - Sincronización finalizada - La sincronización por SMS está habilitada pero debe acceder a ella en cada registro individualmente. + Sincronización completada + La sincronización de SMS está disponible, pero se debe acceder a cada registro. Descargando imágenes... Descripción del programa - Pin erróneo - Inscrito en: + Pin Incorrecto + Inscrito en + Propiedad de Sincronización necesaria - Error de sincroniación + Error en la sincronización Sincronizado Advertencia de sincronización Sincronizando SMS enviado - ¿Quiere mandar todos sus cambios? - ¿Quiere mandar sus cambios de %s? - ¿Quiere mandar sus cambios de este %s? - ¿Quiere comprobar si existen cambios? - ¿Quiere comprobar si existen cambios en %s? - ¿Quiere comprobar si existen cambios para este %s? - ¿Quiere mandar el SMS de nuevo? + ¿Quieres enviar todos tus cambios? + ¿Quieres enviar tus cambios para %s? + ¿Quieres enviar tus cambios para esto %s? + ¿Quieres comprobar si hay actualizaciones? + ¿Quieres comprobar si hay actualizaciones para %s? + ¿Quieres comprobar si hay actualizaciones para este %s? + ¿Quieres volver a enviar el SMS? Enviar Actualizar Ahora no - Toque aquí para explorar - La versión %s de la app está ahora disponible. ¿Quiere instalarla? + Toca aquí para explorar + + %s error + %s errores + %s errores + + + %s advertencia + %s advertencias + %s advertencias + + La versión %s de la app está disponible. ¿Quieres descargarla? Actualización de software Más tarde - Descargar ahora - Buscar actualizaciones + Descarga ahora + Comprobar actualizaciones Buscando actualizaciones No hay nuevas actualizaciones - El permiso para fuentes desconocidas está deshabilitado. Habilítelo para instalar esta versión. - El permiso de almacenamiento está deshabilitado. Habilítelo para instalar esta versión. + El permiso de fuentes desconocidas está denegado.\nDebes habilitarlo para instalar la versión. + El permiso de almacenamiento está denegado.\nDebes habilitarlo para instalar la versión. Versión de la app - Hay algún error en la configuraciónd el programa, no se puede cargar el evento. Por favor contacte a su administrador. - Eliminar persona - Permiso de notificaciones está deshabilitado. - Permiso de notificaciones concedido. - No sincronizaco + Algo salió mal con la configuración del programa, no podemos cargar este evento. Comuníquese con el administrador de su sistema. + Eliminar Persona + El permiso de notificación está denegado. + Permiso de notificación concedido. + No Sincronizado Reintentar sincronización - Marcado para seguimiento - %s atrasado - Registrado en: - Solo vista + Marcado para hacer seguimiento + Atrasado %s + Registrado en + Solo lectura Programado: %s Programado para %s Omitido - Programas: - Sincronización existosa - Aviso en la sincronización - Error de sincronización - Marcar para seguimiento - Eliminar + Programas + Sincronización exitosa + Advertencia de sincronización + Error en la sincronización + Marcar para hacer seguimiento Coordenadas - No tiene acceso a los datos. Contacte con su administrador. - Parece que está offline. Compruebe su conexión. - ¿Eliminar este %s? - Este %s y todos sus datos de todos los programas serán eliminados. Esta acción no puede dehacerse. + No tiene acceso a los datos.\nContacte a su administrador + Parece que está desconectado. Por favor, revise su conexión a Internet + ¿Borrar %s? + Este %s y todos los datos relacionados en los diferentes programs se borrarán. Esta acción no se puede deshacer. ¿Eliminar de %s? - Los datos del %s serán elimiados. Esta acción no puede deshacerse. - Programar el siguiente - Programar - Etapa + Datos de %s se borrarán. Esta acción no se puede deshacer. + Agendar siguiente + Programado + Ingresa + Referir + Planificado + Añade %1$s + Cancela %1$s + Etapa de programa evento - %d eventos Línea de tiempo - Limpiar búsqueda + Borrar búsqueda Opcional Base de datos descargada Importando base de datos - Reabrir el formulario para editarlo + Reabrir el formulario para editar + No hay campos de búsqueda configurados.\nContacte con su administrador por favor. Mostrar más Mostrar menos Sincronizar Mostrar campos Ocultar campos + Importado con éxito + Esta sección está mal configurada.\nContacte con su administrador. + Sincronizado con éxito + Sincronizando %s... + archivos + Mostrar la descripción + Ocultar la descripción + Verifica que eres Tu + Usar Contraseña + Transferir + Transferir %s + De %s a... + Transferencia completada + %1$scancelado + fecha de vencimiento actualizada diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7ff1fef2e9..f83126a6fa 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -94,7 +94,6 @@ Terminer et Compléter Terminé Annulé - rouvrir Activer Actif @@ -449,7 +448,6 @@ Références vides Afin d\'utiliser la connexion par le scanner d\'empreinte digitale vous devez d\'abord vous connecter normalement Sécurité biométrique - Utiliser vos empreintes digitales pour se connecter avec cet utilisateur? Tables Tableau https:// @@ -698,6 +696,13 @@ Nous n\'avons pas pu connecter la session avec votre nom d\'utilisateur Il y a des événements planifiés qui ne seront pas reprogrammés Graphiques + Vue de tableau + Analytiques + Tâches + Programmes + Notes + Saisie de données + Details Afficher comme graphique Afficher comme une ligne Afficher comme tableau diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 2ce6f659b6..ab43e31407 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 dhis @@ -91,7 +91,6 @@ Selesai dan lengkap Selesai Dibatalkan - Buka kembali Aktivasi Aktif @@ -440,7 +439,6 @@ Kredensial Kosong Untuk dapat login menggunakan pemindai sidik jari, Anda terlebih dahulu harus menggunakan cara default Keamanan Biometrik - Gunakan sidik jari Anda untuk masuk dengan pengguna ini? Tabel Tabel https:// @@ -798,6 +796,13 @@ Kami tidak dapat memulai sesi dengan pengguna Anda Ada even yang direncanakan yang tidak akan dijadwalkan ulang Grafik + Tampilan tabel + Analitik + Tugas + Program-program + Catatan + Data entri + Rincian Lihat sebagai Bar Lihat sebagai Garis Lihat sebagai Tabel diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index 6aeb211b45..347e58cf5f 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -1,4 +1,4 @@ - + ឌីអេចអាយអេសធូ (DHIS 2) ឌីអេចអាយអេសធូ (DHIS 2) @@ -71,7 +71,6 @@ បានត្រឹមតែអាន បានបញ្ចប់ បានលុបចោល - បើកឡើងវិញ បញ្ចូន ដើម្បីធ្វើបច្ចុប្បន្នភាព diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml index 87c6643d8d..b76f073786 100644 --- a/app/src/main/res/values-lo/strings.xml +++ b/app/src/main/res/values-lo/strings.xml @@ -1,4 +1,4 @@ - + ລະບົບ DHIS2 ລະບົບຂໍ້ມູນຂ່າວສານດ້ານສຸຂະພາບຂັ້ນເມືອງ @@ -89,7 +89,6 @@ ສີ້ນສຸດ ແລະ ສໍາເລັດ ສໍາເລັດແລ້ວ ຍົກເລີກແລ້ວ - ເປີດຄືນໃໝ່ ເປິດໃຊ້ງານ ທີ່ຍັງນໍາໃຊ້ຢູ່ @@ -435,7 +434,6 @@ ບໍ່ມີຂໍ້ມູນສຳລັບຫ້ອງຂໍ້ມູນນີ້ ຂໍ້ມູນປະຈຳຕົວຫວ່າງເປົ່າ ເພື່ອໃຫ້ສາມາດເຂົ້າສູ່ລະບົບໂດຍໃຊ້ເຄື່ອງສະແກນລາຍນິ້ວມື້ ທ່ານຕ້ອງໃຊ້ວິທີເລີ່ມຕົ້ນກ່ອນ - ໃຊ້ລາຍນິ້ວມືຂອງທ່ານເພື່ອເຂົ້າສູ່ລະບົບກັບຜູ້ໃຊ້ນີ້ບໍ? ຕາຕະລາງຂໍ້ມູນ ຕາຕະລາງຂໍ້ມູນ ຂໍ້ມູນທີ່ບໍ່ໄດ້ຮັບການຊິ້ງຈະສູນເສຍໄປ. ທ່ານຕ້ອງການສືບຕໍ່ຫຼືບໍ? @@ -646,6 +644,12 @@ ລາຍ​ລະ​ອຽດ​ເພີ່ມ​ເຕີມ ປິດ ແຜນວາດຂໍ້ມູນ + ເບີ່ງຕາຕະລາງ + ການວິເຄາະ + ສາຍງານ + ຂໍ້ຄວາມ + ປ້ອນຂໍ້ມູນ + ລາຍລະອຽດ View as Bar ເບິ່ງເປັນແຖວ ເບິ່ງເປັນຕາຕະລາງ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index fc9a264ee6..74c98a7c79 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -90,7 +90,6 @@ Gjør ferdig og fullfør Fullført Kansellert - Åpne igjen Aktiver Aktiv @@ -441,7 +440,6 @@ Tomme opplysninger For å kunne logge inn ved bruk av fingerskanner må du først logge inn på standard måte Biometrisk sikkerhet - Vil du bruke fingerskanner for å logge inn med denne brukeren? Tabeller Tabell https:// @@ -685,6 +683,13 @@ Informasjon om feil Det er planlagte arrangementer som ikke vil bli planlagt på nytt Diagrammer + Tabellvisning + Analyser + Oppgaver + Programmer + Notater + Dataregistrering + Detaljer Vis som tabell Dette året Denne tomåned diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4b27d58b95..7c56b573ea 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -95,7 +95,6 @@ Afwerken en voltooi Voltooid Geannuleerd - heropenen Activeren Actief @@ -453,7 +452,6 @@ Lege inloggegevens Om in te kunnen loggen met de vingerafdrukscanner moet je eerst de standaardmanier gebruiken Biometrische beveiliging - Gebruik je je vingerafdruk om in te loggen met deze gebruiker? Tafels Tafel https:// @@ -465,7 +463,6 @@ Verstuur data Help DHIS2 deze app te verbeteren door foutenlogboeken te delen die zijn gekoppeld aan uw gebruikers-ID. Wil je ons helpen deze app te verbeteren? - Als u ja zegt, stuurt de app anonieme statistieken en foutlogboeken die ons helpen de gebruikerservaring en prestaties te verbeteren.\n\nRaadpleeg ons privacybeleid voor meer informatie. privacybeleid Velden controleren Je internetverbinding is verbroken tijdens het uitvoeren van je configuratiesynchronisatie. Probeer het later opnieuw @@ -705,6 +702,13 @@ We kunnen de sessie met uw gebruiker niet starten Er zijn geplande evenementen die niet opnieuw worden gepland Grafieken + Tabel weergave + Analyse + Taken + Programma\'s + Notities + Gegevensinvoer + Details Bekijk als balk Bekijk als lijn Bekijk als tabel @@ -866,7 +870,6 @@ %s waarschuwing %s waarschuwingen - App-versie %s is nu beschikbaar.\nWilt u deze downloaden? Software-update Later Download nu diff --git a/app/src/main/res/values-prs/strings.xml b/app/src/main/res/values-prs/strings.xml index 8f785365cb..b9c0a19b75 100644 --- a/app/src/main/res/values-prs/strings.xml +++ b/app/src/main/res/values-prs/strings.xml @@ -1,4 +1,4 @@ - + خطا پېښې diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 13efdebeb1..a6ddfe7a6f 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -1,4 +1,4 @@ - + تېروته پېښې diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index dfce0bb006..7610b925e6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,4 +1,4 @@ - + Erro Adicionar novo diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 233e0e95e4..fbc7359f9c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -33,14 +33,19 @@ Erro de sincronização Ocorreu um erro durante a sincronização. Por favor tente novamente. + A sincronização foi concluída, mas não recebemos a confirmação do servidor para todos os registos. Esses registos ainda estão marcados como “offline” na aplicação. Recomendamos que tente novamente a sincronização. FALHOU: Ocorreu um erro no processo de sincronização. Se você tiver conexão, tente novamente. Se o erro persistir, entre em contato com seu administrador. AVISO: A maioria dos seus dados é sincronizada, mas alguns campos apresentaram conflitos. Sincronize sua configuração e tente novamente. Verifique o status da sincronização: programas, TEIs e eventos com conflitos são marcados como @. AVISO: A maioria dos seus dados é sincronizada, mas alguns campos apresentaram conflitos. Sincronize sua configuração e tente novamente. Se o aviso persistir, verifique o status de sincronização: programas, TEIs e eventos com conflitos são marcados como @. ERRO: Alguns dados não foram sincronizados. Verifique o status da sincronização: programas com erros são marcados como @, TEI\'s e eventos como $\". ERRO: Alguns dados não foram sincronizados. Verifique o status da sincronização: TEIs e eventos com conflitos estão marcados como $\". + Ocorreu um erro e o servidor não respondeu. Contacte o seu administrador. + %s resultados encontrados Novo Adicionar + Entrar %s + Introduzir / Saltar %s Nova agenda Transferência @@ -58,6 +63,8 @@ Falha na sincronização da configuração Falha na sincronização de dados Recuperar Conta + É necessária uma ligação à rede para recuperar a sua conta. + Diário Semanal @@ -71,6 +78,9 @@ Pesquisar + Pesquisar / Adicionar novo %s + Procurar por %s + Adicionar novo %s Inserir texto Inserir texto longo Inserir numeros @@ -87,11 +97,14 @@ Programa inativo Abrir + Não concluído + Evento concluído Apenas leitura Terminar e concluir Completo Cancelado - reabrir + Reabrir + Reabrir o formulário Activar Activo @@ -131,12 +144,17 @@ Não há períodos disponíveis para seleção. Data de vencimento + Próximo %1$s Combinação de categoria DD/MM/AAAA Cancelar Apagar Total Data do evento mais recente + Nenhuma opção disponível + + + Lista de inscrições Programas activos Programas para inscrição Registrar @@ -146,17 +164,23 @@ Veja detalhes VEJA MAIS + Editar Acompanhamento Acompanhamento ativado Acompanhamento desativado Desactivar Este programa não permite mais eventos + + Sem dados eventos Início Informações do aplicativo Sair Bloquear Sessão + Mostrar%s mais… + Mostrar menos + 1 Minuto 15 Minutos @@ -183,6 +207,7 @@ Módulo SMS: Porta de entrada SMS: remetente do resultado SMS + Tempo limite do resultado SMS (seg) segundos número número @@ -215,6 +240,10 @@ Sua configuração e dados do aplicativo armazenados no dispositivo serão excluídos. Você será solicitado a fazer login novamente e os dados que não forem sincronizados com o servidor serão perdidos. Configurações de SMS Verifique e edite os parâmetros relacionados ao porta de entrada sms + Atualização do software + Software + Exportar base de dados + Exporte a sua base de dados e partilhe-a com o seu administrador Data de inscrição @@ -232,6 +261,7 @@ Faltam campos obrigatórios Alguns campos obrigatórios estão ausentes. Se você quiser voltar agora, todas as informações salvas serão excluídas. Alguns campos obrigatórios estão ausentes. Se você quiser voltar, por favor preencha-os. + Alguns campos requerem a sua atenção.\nDeseja rever o formulário? Seus dados não são salvos Se você voltar agora, todas as informações inseridas serão excluídas. Suprimir e voltar @@ -358,10 +388,12 @@ Selecione as datas para filtrar Selecionar opção Problema relatado + Gerar novo evento Deseja criar outro evento? Eventos Concluídos Todos os eventos deste programa estão concluídos. Deseja encerrar o programa também? Deseja concluir a inscrição também? + Gostaria de completar também as %s Verifica Início @@ -413,6 +445,9 @@ Não há indicadores para este programa. Notas Não foi possível concluir + Não é possível guardar + Existem avisos no formulário + O que quer fazer? Alguns campos possuem erros. Por favor, verifique-os para poder concluir Todas as inscrições Mostrar painel com todos os programas ativos @@ -443,7 +478,8 @@ Credenciais Vazias Para poder fazer login usando o scanner de impressão digital você precisa primeiro usar a maneira padrão Segurança biométrica - Use sua impressão digital para fazer login com este usuário? + Início de sessão rápido + Gostaria de utilizar a sua impressão digital ou outro método biométrico para iniciar sessão? Tabelas Tabela https:// @@ -454,6 +490,9 @@ Pelo menos uma unidade organizacional deve ser selecionada Enviar dados Ajude o DHIS2 a melhorar este aplicativo compartilhando logs de erros associados ao seu ID de usuário. + Quer nos ajudar a melhorar esta aplicação? + Se disser que sim, a aplicação enviará estatísticas anónimas e registos de erros que nos ajudam a melhorar a experiência e o desempenho do utilizador.\n\nPara mais informações, consulte a nossa política de privacidade. + política de privacidade. Verificar campos Você perdeu sua conexão com a Internet ao executar sua sincronização de configuração. Por favor tente novamente mais tarde Aceita @@ -467,7 +506,9 @@ O registro que você está tentando acessar não está disponível. Deseja procurar? Sincronizado por SMS Advertência + A carregar Relação + Enviado por SMS Não há conflitos para este programa A conexão de rede não está disponível, mas parece que existem serviços telefônicos - você pode usar o SMS. A conexão de rede não está disponível. @@ -489,6 +530,7 @@ Configuração de sincronização: %s O registro que você está tentando acessar não tem inscrição. Você não pode acessar o painel. Erro de formatação + Apagar%s Você não tem autoridade para suprimir este registro Este url não é válido Ajustando formulário... @@ -663,6 +705,7 @@ O selecionado %s não pôde ser baixado. Completado por %s: %s Atualizado: %s + Feito Não foi possível verificar a qualidade dos dados. Contate seu administrador Tem certeza? Você deseja reabrir o conjunto de dados? @@ -673,6 +716,7 @@ Clique aqui para conferir nosso política de privacidade Os valores das coordenadas não são válidos Você não pode inscrever TEIs de outras unidades organizacionais neste programa porque está protegido.\nContate seu administrador. + Não é possível inscrever um %s de outras unidades organizacionais neste programa porque está protegido.\nContacte o seu administrador. Este campo é obrigatório Por razões de segurança, esta versão do aplicativo não pode ser usada com o adb. Por favor, use a versão de treinamento. @@ -689,35 +733,254 @@ Não foi possível fazer login com seu nome de usuário Existem eventos planejados que não serão reprogramados Gráficos + Lista + Vista de tabela + Mapa + Google Analytics + Tarefas + Programas + Gráficos + Relações + Notas + Entrada de dados + Formulário + Detalhes Ver como barra Ver como linha Ver como tabela Ver como valor Ver como gráfico circular + Lista de trabalho %s + Últimos %d dias Este ano Este bimensal Ultimo bimensal Este Trimestre Último trimestre + Estes seis meses + Últimos seis meses + Últimos %d anos + últimos %d meses + últimas %d semanas Semanas deste ano Meses deste ano Bimensais deste ano Trimestres deste ano Meses do ano passado + Trimestres dos últimos anos Último ano + Estes quinze dias + Últimos quinze dias + Últimos %d bimês + Últimos %d trimestres + últimos %d meses Ano financeiro Último ano fiscal + Últimos %d anos financeiros + Últimas %d quinzenas Retroinformacão Indicadores + A permissão da câmara é negada.\nPrecisa de a ativar para utilizar esta funcionalidade. Valor + Gráficos e indicadores + Tirar fotografia + Selecionar a partir da galeria Feito! + Este programa está protegido + Descreva aqui o motivo + Autenticando + O URL do servidor fornecido não é uma instância dhis Credenciais erradas + Servidor desconhecido Valor deve ser um número inteiro + A sessão expirou + A sua sessão atual expirou. Por motivos de segurança, terá de fornecer novamente as suas credenciais Valores + Semana %d %s a%s + Não voltar a mostrar + Configuração de funções Mostrar como radar + Não é possível definir o tipo de coordenadas + Seguimento%s + %s não pode ser analisado como uma data + Não foi possível atualizar este campo. Por favor, tente novamente + Edição Disponível + Estes dados não são editáveis porque estão marcados como concluídos + Estes dados não são editáveis porque o seu tempo de edição expirou + Não tem permissão para editar estes dados + Estes dados não são editáveis porque a unidade organizacional está encerrada + Não tem permissão para editar estes dados + Não é possível editar dados desta unidade organizacional + Repor a pesquisa + A pesquisa em linha deu a seguinte mensagem: + Aviso de regras do programa + Existe um problema de configuração que está a provocar um ciclo nas regras. Contacte o seu administrador. + ACTUALIZAR Desconhecido + Atualizar todos os dados +  Atingiu o número máximo de contas (%d). Para adicionar uma nova conta, inicie sessão e elimine uma das suas contas + Adicionar conta + Eliminar conta + Gerir contas + Resolução de problemas de configuração Idioma + Toque aqui para alterar o idioma da aplicação + Validação de regras de programa + Esta ação verificará todas as regras configuradas e apresentará as configurações incorrectas, caso existam + Validação das regras do programa... + Não foram encontrados avisos + Tudo parece óptimo + Não guardado + Alguns campos requerem a sua atenção.\nDeseja rever o formulário? + Carregar mais resultados + Não há mais resultados para %s + Pesquisar fora deste programa + Não há mais resultados + Procurar mais resultados + Não existem resultados para esta pesquisa + Muitos resultados + Tente alterar a sua pesquisa para restringir os resultados. Crie um novo + Novo %s + É necessário introduzir pelo menos %s atributos para pesquisar Esta bem - + Iniciar uma pesquisa para encontrar qualquer %s + É possível pesquisar ou criar um novo %s + Tem algumas mensagens de aviso. + Tem algumas mensagens de aviso.\nDeseja marcar este formulário como completo? + Alguns campos têm erros e não são guardados. \Deseja rever o formulário? + Deseja marcar este formulário como completo? + Tem algumas mensagens de erro. Quer revê-las? + Se sair agora, as alterações serão rejeitadas. + Reaberto + Conta desactivada + A sua conta foi desactivada por um administrador. + Para pesquisar fora deste programa, abra o formulário de pesquisa e preencha um dos seguintes campos %s : + Versão de construção: %s + Data de construção: %s + Data actual: %s + Dispositivo: %s + Versão do sistema operativo: %s + Abrir detalhes de %s + %s detalhes + A sincronizar %s… + SMS enviado. A aguardar resposta do servidor. + SMS confirmado para chegar ao servidor. Actualização do estado + Não podemos confirmar se o SMS chegou ao servidor. Actualização do estado + SMS enviado. Atualização do estado. + SMS não enviado. + Abrir a aplicação SMS. Não modificar a mensagem, por favor. + Enviar Sms com + Enviou a sms? + A aguardar confirmação manual + Sincronização de eventos + Sincronização de entidades monitorizadas + Sincronização de conjuntos de dados + Sincronização de recursos + Sincronização efectuada + A sincronização de SMS está disponível, mas tem de ser acedida para cada registo individual + Descarregar imagens... + Descrição do programa + Pin errado + Inscrito em + Propriedade de + Sincronização necessária + Erro de sincronização + Sincronizado + Aviso de sincronização + A sincronizar + SMS enviado + Pretende enviar todas as suas alterações? + Pretende enviar as suas alterações para %s? + Deseja enviar as suas alterações para este %s + Pretende verificar se existem actualizações? + Pretende verificar se existem actualizações em %s? + Pretende verificar se existem actualizações para este %s? + Pretende voltar a enviar a SMS? + Enviar + Actualizar + Não agora + Toque aqui para explorar + + %s erro + %s erros + %s Erros + + + %s aviso + %s avisos + %s Alertas + + A versão %s aplicação já está disponível. Quer baixá-lo? + Atualização do software + Mais tarde + Descarregar agora + Verifique se existem atualizações + Verificação de actualizações + Não há novas actualizações + A permissão de fontes desconhecidas foi negada. É necessário activá-la para instalar a versão. + A permissão de armazenamento foi negada. É necessário activá-la para instalar a versão. + Versão da aplicação + Algo correu mal com a configuração do programa, não é possível carregar este evento. Contacte o administrador do sistema. + Eliminar pessoa + A permissão de notificação negada. + Autorização de notificação concedida. + Não sincronizado + Repetir a sincronização + Marcado para acompanhamento + Atrasado %s + Registado em + Ver apenas + Programado: %s + Programado para %s + Pulado + Programas + Sincronização bem sucedida + Aviso de sincronização + Erro de Sincronização + Marcar para acompanhamento + Coordenadas + Não tem acesso aos dados.\nContacte o seu administrador. + Parece que está offline. Por favor, verifique a sua ligação + Eliminar este %s? + Este%s e todos os seus dados em todos os programas serão eliminados. Esta ação não pode ser anulada. + Remover de %s? +  Os dados de %s serão eliminados. Esta ação não pode ser anulada. + Agendar próximo + Cronograma + Entrar + Referir + Agendado + Entrar %1$s + Cancelar %1$s + Estagio de programa + evento + Linha do tempo + Limpar pesquisa + Opcional + Banco de dados descarregado + Importando base de dados + Reabrir o formulário para editar + Não existem campos de pesquisa configurados para utilização. \nContacte o seu administrador. + Mostre mais + Mostre menos + Sincronizar + Mostrar campos + Ocultar campos + Importação bem sucedida + Esta secção está mal configurada.\nContacte o seu administrador. + Sincronização bem sucedida + A sincronizar %s… + recursos de ficheiros + Mostrar descrição + Ocultar descrição + Verifique se é você + Usar palavra-passe + Transferir + Transferir %s + De %s para… + Transferido com sucesso + %1$s cancelado + Data de vencimento actualizada + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ff0b1b0daa..fcef6b457d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,4 +1,4 @@ - + Ошибка Что-то пошло не так @@ -169,6 +169,11 @@ Вернуться Закрыть Диаграммы + Аналитика + Программы + Заметки + Ввод данных + Детали Этот год Этот бимесяц Прошлый бимесяц @@ -200,7 +205,6 @@ Пропущенный Программы Отметить для последующего наблюдения - Убрать Координаты Расписание Этап программы diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index af86c2d7f2..ff803457aa 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 DHIS @@ -69,7 +69,6 @@ read only Avklarad Avbruten - Öppna igen Synkronisera Att uppdatera diff --git a/app/src/main/res/values-tet/strings.xml b/app/src/main/res/values-tet/strings.xml index aa46592701..47c2110c08 100644 --- a/app/src/main/res/values-tet/strings.xml +++ b/app/src/main/res/values-tet/strings.xml @@ -1,4 +1,4 @@ - + Erru Aumenta foun diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml index 0e8cb3991f..3235354b51 100644 --- a/app/src/main/res/values-tg/strings.xml +++ b/app/src/main/res/values-tg/strings.xml @@ -1,4 +1,4 @@ - + Хатогӣ Рӯйдодҳо diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index bae9cb6155..f4203ee95b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -99,7 +99,6 @@ Закінчити і завершити Завершено Скасовано - повторно відкрити Активувати Активно @@ -466,7 +465,6 @@ Порожні облікові дані Щоб мати можливість увійти за допомогою сканера відбитків пальців, спочатку потрібно використовувати спосіб за замовчуванням Біометрична безпека - Використовувати відбиток пальця, щоб увійти під цим користувачем? Таблиці Таблиця https:// @@ -717,6 +715,13 @@ Нам не вдалося почати сесію із Вашим користувачем Є заплановані випадки, які не будуть перенесені Діаграми + У вигляді таблиці + Аналітика + Завдання + Програми + Примітки + Введення даних + Деталі Переглянути як стовпчикову діаграму Переглянути як лінійний графік Переглянути як таблицю @@ -856,7 +861,6 @@ Програми Помилка синхронізації Позначити для подальших дій - Видалити Запланувати наступний Запланувати Етап програми diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index b282373b81..b533db429c 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1,4 +1,4 @@ - + غلطی میٹا ڈیٹا diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 706614f5b0..8cbdb3a0f6 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 DHIS @@ -91,7 +91,6 @@ Якунлаш ва тўлдириш Тўлдирилди Бекор қилинди - қайта очиш Фаоллаштириш Фаол @@ -441,7 +440,6 @@ Ишонч ёрлиқлари бўш Бармоқ излари сканери ёрдамида тизимга кириш учун аввал стандарт усулдан фойдаланишингиз керак Биометрик Хавфсизлик - Фойдаланувчи тизимга кириши учун бармоқ изларидан фойдаланилсинми? Жадваллар Жадвал https:// @@ -689,6 +687,13 @@ Сизнинг фойдаланувчингиз билан сеанс ташкиллаштира олмадик Режалаштирила олинмайдиган ҳодиса/тадбир режалари мавжуд Диаграммалар + Жадвал кўриниши + Аналитика + Вазифалар + Дастурлар + Изоҳлар + Маълумот киритиш + Тафсилотлари Панел сифатида кўриш Қатор сифатида кўриш Жадвал сифатида кўриш diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7e4633e181..4e66bcd454 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,4 +1,4 @@ - + Dhis2 dhis @@ -42,6 +42,8 @@ Tìm thấy %s kết quả Thêm sự kiện mới Thêm + Nhập %s + Nhập / Bỏ qua %s Đặt lịch hẹn Giới thiệu đến cơ sở khác @@ -59,6 +61,8 @@ Đồng bộ Cấu Hình thất bại Đồng bộ Dữ Liệu thất bại Phục hồi tài khoản + Cần có kết nối mạng để phục hồi tài khoảng của bạn + Hàng Ngày Hàng Tuần @@ -68,18 +72,21 @@ Chấp nhận Chia sẻ Chia sẻ %d - Chia sẻ %d + Chia sẻ %d Tìm kiếm + Tìm kiếm / Thêm mới %s + Tìm %s + Thêm mới %s Nhập văn bản Nhập văn bản dài Nhập số Nhập số nguyên Nhập số nguyên âm Nhập số nguyên dương hoặc số 0 - Nhập số nguyên - Các Tùy chọn bộ lọc + Nhập số nguyên dương + Tùy Chọn Bộ Lọc Chọn ngày Thay đổi lịch @@ -88,11 +95,14 @@ Chương trình không hoạt động Mở + Chưa hoàn tất + Sự kiện đã hoàn tất Chỉ Xem Lưu và Hoàn Tất - Hoàn tất + Đã hoàn tất Đã hủy - mở lại + Mở lại + Mở lại biểu nhập Kích hoạt Hoạt động @@ -114,8 +124,8 @@ Cập nhật Ngày Đơn vị - Không có bất cứ Đơn Vị nào trong ngày được chọn. Xin vui lòng chọn ngày khác hoặc Đơn Vị khác. - Tùy Chọn + Không có Đơn Vị nào trong ngày được chọn. Xin vui lòng chọn ngày khác hoặc Đơn Vị khác. + Tùy Chọn Phân Loại Vĩ độ Kinh độ Sự kiện mới @@ -132,40 +142,51 @@ Không có thời điểm để chọn. Ngày đến hạn + %1$skế tiếp Tổ hợp Phân Loại DD/MM/YYYY Hủy Xóa Tổng số Ngày sự kiện gần nhất + Không có tùy chọn + + + Danh sách đăng ký Các chương trình đang hoạt động Chương trình để đăng ký Đăng ký - Ngày đến khám gần nhất + Ngày đến khám gần nhất : áp dụng Xem Chi Tiết XEM THÊM + Chỉnh sửa Theo dõi Đã bật theo dõi Đã tắt theo dõi Không hoạt động Chương trình này không cho phép thêm sự kiện + + Không có dữ liệu sự kiện Trang chủ Thông Tin Ứng Dụng Đăng Xuất - Khóa Phiên + Khóa Phiên đăng nhập + Hiển thị thêm %s... + Ẩn bớt... + 1 Phút 15 Phút 30 Phút - 1 Tiếng - 6 Tiếng + 1 Giờ + 6 Giờ 1 Ngày (Mặc định) - 12 Tiếng + 12 Giờ Đồng bộ dữ liệu tự động mỗi: Thủ công Tự động đồng bộ cấu hình mỗi: @@ -184,6 +205,7 @@ Mô đun SMS: Cổng SMS Người gửi kết quả SMS + Thời gian chờ kết quả SMS (giây) giây số số @@ -214,6 +236,10 @@ Cấu hình và dữ liệu trong ứng dụng của bạn được lưu trữ trong thiết bị sẽ bị xóa. Bạn sẽ được yêu cầu đăng nhập lại và dữ liệu chưa đồng bộ lên máy chủ sẽ bị mất. Cài đặt SMS Kiểm tra và chỉnh sửa các tham số liên quan tới cổng sms + Cập nhật phần mềm + Phần mềm + Xuất cơ sở dữ liệu + Xuất cơ sở dữ liệu và chia sẻ nó với quản trị của bạn Ngày đăng ký @@ -358,10 +384,12 @@ Chọn ngày để lọc Tùy chọn Vấn đề đã được báo cáo + Tạo sự kiện mới Bạn có muốn tạo một sự kiện khác? Sự kiện đã hoàn tất Tất cả các sự kiện trong chương trình này đã hoàn tất. Bạn có muốn đóng chương trình không? Bạn có muốn hoàn tất đăng ký? + Bạn có muốn hoàn tất %sluôn không? Kiểm tra Trang chủ @@ -446,7 +474,8 @@ Xóa hết thông tin đăng nhập Để có thể đăng nhập bằng cách sử dụng máy quét dấu vân tay, trước tiên bạn cần phải sử dụng cách mặc định. Bảo mật sinh trắc học - Sử dụng dấu vân tay để đăng nhập với tên người dùng này phải không? + Đăng nhập nhanh + Bạn có muốn sử dụng dấu vân tay của bạn hoặc phương thức sinh học khác để đăng nhập không? Bảng Bảng https:// @@ -457,6 +486,9 @@ Phải chọn ít nhất một đơn vị Đã gửi dữ liệu Giúp DHIS2 cải thiện ứng dụng này bằng việc chia sẻ lịch sử lỗi liên kết với tên đăng nhập của bạn. + Bạn có muốn giúp chúng tôi cải thiện ứng dụng này không? + Nếu bạn đồng ý, ứng dụng sẽ gửi nhật ký thống kê vô danh và lỗi để giúp chúng tôi cải thiện trải nghiệm và vận hành của người dùng. \n\nĐể biết thêm thông tin chi tiết, xin vui lòng tham khảo chính sách bảo mật của chúng tôi. + chính sách bảo mật Kiểm tra các trường dữ liệu Bạn đã mất kết nối mạng trong khi đang thực hiện đồng bộ cấu hình. Vui lòng thử lại sau. Đồng ý @@ -669,6 +701,7 @@ %s đã chọn không thể tải xuống Đã hoàn tất được %s: %s Đã cập nhật: %s + Hoàn thành Chúng tôi không thể kiểm tra chất lượng dữ liệu. Hãy liên hệ quản lý của bạn. Bạn có chắc chắn không? Bạn có muốn mở-lại biểu nhập này không? @@ -696,6 +729,18 @@ Chúng ta không thể khởi đầu phiên đăng nhập bằng tên đăng nhập của bạn Có các sự kiện đã lên kế hoạch mà sẽ không chỉnh sửa lịch hẹn. Biểu đồ + Danh sách + Xem bảng + Bản Đồ + Phân tích + Các nhiệm vụ + Chương trình + Biểu Đồ + Các mối quan hệ + Ghi Chú + Nhập dữ liệu + Biểu nhập + Chi tiết Xem Cột Xe Đường Xem Bảng @@ -793,10 +838,12 @@ Có quá nhiều kết quả Thử thay đổi tìm kiếm của bạn để thu hẹp kết quả. Tạo mới + %sMới Cần phải nhập ít nhất %sthuộc tính để tìm kiếm Ok Mở trang tìm kiếm để tìm bất cứ %snào Bạn có thể tìm kiếm hoặc tạo một %smới + Bạn có một số tin nhắn cảnh báo. Bạn có một số tin nhắn cảnh báo. \n Bạn có muốn đánh dấu biểu nhập này là hoàn tất không? Một số trường dữ liệu có lỗi và chưa được lưu. \n Bạn có muốn xem xét biểu nhập không? Bạn có muốn đánh dấu biểu nhập này là hoàn tất không? @@ -812,4 +859,120 @@ Thiết bị: %s Phiên bản hệ điều hành: %s Mở chi tiết %s - + Chi tiết %s + Đang đồng bộ %s... + Đã gửi SMS. Đang đợi máy chủ trả lời + Đã xác nhận SMS đến máy chủ. Đang cập nhật Trạng Thái + Chúng tôi không thể xác nhận SMS đến máy chủ hay chưa. Đang cập nhật trạng thái + Đã gửi SMS. Đang cập nhật trạng thái. + Chưa gửi SMS. + Đang mở ứng dụng SMS. Xin vui lòng không chỉnh sửa tin nhắn. + Gửi SMS với + Bạn đã gửi SMS chưa? + Đang đợi xác nhận thủ công + Đang đồng bộ các sự kiện + Đang đồng bộ các đối tượng theo dõi + Đang đồng bộ các tập dữ liệu + Đang đồng bộ tài nguyên + Đồng bộ hoàn tất + Đồng bộ SMS đang hiển thị nhưng phải được truy suất từng hồ sơ cá nhân + Đang tải hình ảnh... + Mô tả chương trình + Sai mã pin + Đã đăng ký trong + Sở hữu bởi + Cần được đồng bộ + Lỗi đồng bộ + Đã đồng bộ + Cảnh báo đồng bộ + Đang đồng bộ + Đã gửi SMS + Bạn có muốn gửi tất cả những thay đổi của bạn không? + Bạn có muốn gửi các thay đổi của bạn về %s không? + Bạn có muốn gửi các thay đổi của bạn đối với %snày không? + Bạn có muốn kiểm tra các cập nhật không? + Bạn có muốn kiểm tra các cập nhận trên %s không? + Bạn có muốn kiểm tra các cập nhật đối với %s này không? + Bạn có muốn gửi SMS lại không? + Gửi đi + Làm mới + Không phải bây giờ + Bấm vào đây để xem thêm + + Các lỗi %s + + + Các cảnh báo %s + + Phiên bản ứng dụng %s đang có. Bạn có muốn tải nó không? + Cập nhật phần mềm + Để sau + Tải xuống ngay + Kiểm tra các cập nhật + Đang kiểm tra các cập nhật + Không có cập nhật mới + Quyền nguồn không xác định bị từ chối.\n Bạn cần cho phép nó để cài đặt. + Quyền lưu trữ bị từ chối. \n Bạn cần cho phép để cài phiên bản. + Phiên bản ứng dụng + Có lỗi trong cấu hình chương trình, chúng tôi không thể tải sự kiện này. Xin vui lòng liên hệ quản trị hệ thống của bạn. + Xóa người + Quyền thông báo bị từ chối. + Quyền thông báo đã được cấp. + Chưa được đồng bộ + Đồng bộ lại + Đã đánh dấu để theo dõi + Hết hạn %s + Được đăng ký trong + Chỉ xem + Đã đặt lịch hẹn: %s + Đã xếp lịch hen đến %s + Đã bỏ qua + Chương trình + Đồng bộ thành công + Cảnh báo đồng bộ + Lỗi đồng bộ + Đánh dấu để theo-dõi + Toạ độ + Bạn không có quyền truy cập dữ liệu.\n Liên lạc quản trị của bạn. + Dường như bạn đang ngoại tuyến. Xin vui lòng kiểm tra kết nối mạng + Xóa %s này? + %snày và tất cả dữ liệu của nó giữa tất cả chương trình sẽ bị xóa. Hành động này không thể hồi phục. + Loại bỏ khỏi %s? + Dữ liệu từ %ssẽ bj xóa. Hành động này không thể hồi phục. + Lịch hẹn kế tiếp + Lịch trình + Nhập + Tham chiếu + Đã xếp lịch + Nhập %1$s + Hủy %1$s + Giai Đoạn Chương Trình + sự kiện + Dòng thời gian + Bỏ tìm kiếm + Không bắt buộc + Cơ sở dữ liệu đã được tải về + Đang nhập cơ sở dữ liệu + Mở lại form để chỉnh sửa + Không có các trường tìm kiếm được cấu hình để sử dụng.\nXin vui lòng liên lạc với quản trị của bạn. + Hiện thêm + Ẩn bớt + Đồng bộ + Hiện trường dữ liệu + Ẩn trường dữ liệu + Nhập thành công + Phần này được cấu hình sai.\nXin vui lòng liên lạc quản trị của bạn. + Đồng bộ thành công + Đang đồng bộ %s... + tài nguyên tập tin + Hiển thị mô tả + Ẩn mô tả + Xác nhận đó là bạn + Sử dụng mật khẩu + Chuyển + Chuyển %s + Từ %s đến ... + Đã chuyển thành công + %1$s đã hủy + Ngày đến hạn đã được cập nhật + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3eb006e2ae..f482b1e1a7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 区域健康信息系统 @@ -89,7 +89,6 @@ 完成 已完成 已取消 - 重新打开 激活 活跃的 @@ -440,7 +439,6 @@ 空证书 为了能够使用指纹扫描器,你需要先使用缺省的方法 生物特征安全 - 使用你的指纹登录该用户? https:// @@ -686,6 +684,13 @@ 我们无法与您的用户发起会话 已经计划的事件不会重新安排 图表 + 表格检视 + 分析 + 任务 + 项目 + 笔记 + 数据输入 + 详情 查看柱状图 查看折线图 查看表格 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 7af761f454..2296cbfe9d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,4 +1,4 @@ - + DHIS2 区域健康信息系统 @@ -20,7 +20,7 @@ 现在同步 与服务器同步中 - 同步 + 现在同步 元数据 事件 @@ -32,8 +32,8 @@ 同步错误 同步中发生错误,请重试 同步完成,但我们没有收到所有记录的服务器确认。这些记录在应用程序中仍被标记为“离线”状态。我们建议重试同步。 - 失败:同步过程中出了点问题。如果您有连接,请重试。如果错误仍然存在,请与管理员联系。 - 警告:大多数数据已经同步,但是一些自动出现冲突,同步配置再试。检查你的同步状态:冲突的项目、跟踪的实体和事件标记为@ + 失败:同步过程中出了问题。如果有连接,请重试。如果错误仍然存在,请联系您的管理员。 + 警告:大部分数据已同步,但某些字段出现冲突。同步配置后再试一次。检查它们的同步状态:有冲突的程序、TEI 和事件会被标记为 @。 警告:你的大多数数据已经同步,但是有些字段存在冲突,同步你的配置再试。如果警告仍然存在,检查你的同步状态:项目、跟踪实体和事件如果有冲突会标记为@。 错误:你有些数据同步失败。检查你的同步状态:项目、跟踪实体和事件如果有冲突会标记为@。 错误:同步数据失败。检查同步状态:所有冲突的实体和事件标记为$\" 。 @@ -42,8 +42,10 @@ 发现结果:1%s 新增 添加 + 输入%s + 输入/跳过 %s 新调度 - 调转 + 转派 服务器URL @@ -74,6 +76,9 @@ 搜索 + 搜索/添加新的 %s + 搜索 %s + 添加新的 %s 输入文本 输入长文本 输入数字 @@ -90,11 +95,13 @@ 项目不活跃 开放 + 没完成 + 事件完成 只读 完成 已完成 已取消 - 重新打开 + 重新打开表单 激活 活跃的 @@ -140,6 +147,10 @@ 删除 总共 最近事件日期 + 无可用选项 + + + 注册清单 活动项目 注册的项目 注册 @@ -149,17 +160,23 @@ 查看详情 更多 + 编辑 后续跟踪 使能后续跟踪 后续跟踪关闭 去活 该项目不允许更多事件 + + 无数据 事件 主页 应用信息 退出 阻止会话 + 显示%s更多… + 显示较少… + 1分钟 15分钟 @@ -186,6 +203,7 @@ 短信模块: 短信网关 短信结果发送者 + SMS 结果超时(秒) 数量 数量 @@ -216,6 +234,10 @@ 所有存储在你的设备的采集app数据和配置将被删除。你将被要求重新登录并且没有与服务器同步的数据将会丢失。 短信设置 检查和编辑短信网关的相关参数。 + 软件更新 + 软件 + 导出数据库 + 导出数据库并与管理员共享 注册日期 @@ -360,10 +382,12 @@ 选择过滤日期 选择选项 报告的问题 + 生成新事件 你要创建另外的事件? 完成的事件 该项目的所有事件已经完成。你同时要关闭该项目吗? 您还想完成注册吗? + 您也想完成%s吗? 检查 主页 @@ -448,7 +472,8 @@ 空证书 为了能够使用指纹扫描器,你需要先使用缺省的方法 生物特征安全 - 使用你的指纹登录该用户? + 快速登录 + 您想使用指纹或其他生物识别方法登录吗? https:// @@ -459,6 +484,9 @@ 至少得选择一个机构 发送数据 通过共享与您的用户ID相关的错误日志来帮助DHIS2改进此应用程序。 + 你想帮助我们改进这个应用程序吗? + 如果您同意,应用程序将发送匿名统计数据和错误日志,帮助我们改善用户体验和性能。 + 隐私政策 检查字段 你丢掉了互联网连接,当执行配置同步的时候。稍后再试。 同意 @@ -671,6 +699,7 @@ 选择的%s不能下载 %s完成:%s 已更新:%s + 完成 不能检查质量,联系管理员 你确信? 你要重新打开数据集? @@ -698,6 +727,15 @@ 我们无法与您的用户发起会话 还有计划的事件,不能重新安排 图表 + 表格显示 + 分析工具 + 任务 + 项目 + 可视化图表应用 + 关系 + 笔记 + 数据输入 + 详情 查看柱状图 查看折线图 查看表格 @@ -795,10 +833,12 @@ 结果太多 尝试更改搜索以缩小结果范围。 新建 + 新%s 需要至少输入%s属性才能搜索 Ok! 开始搜索以找到任何 %s 您可以搜索或创建一个新的 %s + 您收到了一些警告信息。 您有一些警告消息。\n您想将此表单标记为完成吗? 一些字段有错误并且它们没有被保存。 \n您要查看表单吗? 您想将此表单标记为已完成吗? @@ -814,6 +854,7 @@ 设备:%s 操作系统版本:%s 打开 %s 详细信息 + %s 细节 正在同步 %s… 短信发送。等待服务器响应。 短信确认到达服务器。更新状态 @@ -822,5 +863,100 @@ 短信未发送。 打开短信应用。请不要修改留言。 发送短信 + 你发短信了吗? 等待手工确认 + 同步事件 + 同步跟踪的实体 + 同步数据集 + 同步资源 + 同步完成 + SMS 同步可用,但必须为每个单独的记录访问 + 正在下载图片... + 项目说明 + 错误的密码 + 已加入 + 拥有者 + 需要同步 + 同步错误 + 已同步 + 同步警告 + 同步 + 短信已发送 + 您要发送所有更改吗? + 您要发送对 %s 的更改吗? + 您要发送对此 %s 的更改吗 + 您要检查更新吗? + 您要检查 %s 上的更新吗? + 您要检查此 %s 的更新吗? + 是否要再次发送短信? + 发送 + 刷新 + 现在不要 + 点击此处探索 + + %s 错误 + + + %s 警告 + + 应用程序版本%s 现已推出。您想下载吗? + 软件更新 + 之后 + 现在下载 + 检查更新 + 查询更新 + 没有新的更新 + 未知来源权限被拒绝。\n您需要启用它才能安装版本。 + 存储权限被拒绝。\n您需要启用它才能安装版本。 + 应用版本 + 程序配置出了问题,我们无法加载此事件。请联系您的系统管理员。 + 删除人员 + 通知权限被拒绝。 + 已授予通知许可。 + 未同步 + 重试同步 + 标记为后续 + 逾期%s + 已登记 + 只读 + 已安排:%s + 预定%s + 跳过 + 项目 + 同步成功 + 同步警告 + 同步错误 + 后续标记 + 坐标 + 您无权访问数据。\n请与您的管理员联系。 + 您似乎已离线。请检查您的连接 + 删除此%s? + 此 %s 及其所有程序中的所有数据都将被删除。此操作无法撤消。 + 从 %s 中删除? + %s 中的数据将被删除。此操作无法撤消。 + 安排下一个 + 调度 + 项目阶段 + 事件 + 时间线 + 清除搜索 + 可选项描述 + 数据库已下载 + 导入数据库 + 重新打开表单进行编辑 + 没有配置可用的搜索字段,请联系您的管理员。 + 更多 + 显示较少 + 同步 + 显示字段 + 隐藏字段 + 导入成功 + 此部分配置错误。 + 成功同步 + 正在同步 %s… + 文件资源 + 显示说明 + 隐藏说明 + 确认是您 + 使用密码 diff --git a/app/src/release/java/org/dhis2/data/appinspector/AppInspector.kt b/app/src/release/java/org/dhis2/data/appinspector/AppInspector.kt deleted file mode 100644 index b485593805..0000000000 --- a/app/src/release/java/org/dhis2/data/appinspector/AppInspector.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.dhis2.data.appinspector - -import android.content.Context -import okhttp3.Interceptor - -class AppInspector(private val context: Context) { - var flipperInterceptor: Interceptor? = null - - fun init(): AppInspector { - return this - } -} diff --git a/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt b/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt index 1e71a87ae0..01d2178e1f 100644 --- a/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt +++ b/app/src/test/java/org/dhis2/bindings/DateExtensionsTest.kt @@ -25,7 +25,7 @@ class DateExtensionsTest { @Test fun `Should return empty when date is null`() { - val date: Date? = null + val date = null assert(date.toDateSpan(context) == "") assert(date.toUiText(context) == "") } @@ -33,7 +33,7 @@ class DateExtensionsTest { @Test fun `Should return date format when date is after today`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, 1) }.time assert(date.toDateSpan(context, currentDate) == dateFormat.format(date)) @@ -41,16 +41,16 @@ class DateExtensionsTest { } @Test - fun `Should return "now" when date is less than a minute from current date`() { - val date: Date? = Date() + fun `Should return now when date is less than a minute from current date`() { + val date = Date() whenever(context.getString(R.string.interval_now)) doReturn "now" assert(date.toDateSpan(context) == "now") } @Test - fun `Should return "5 minutes ago" when date is 5 minutes ago from current date`() { + fun `Should return 5 minutes ago when date is 5 minutes ago from current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.MINUTE, -5) }.time whenever(context.getString(R.string.interval_minute_ago)) doReturn "%d min. ago" @@ -58,9 +58,9 @@ class DateExtensionsTest { } @Test - fun `Should return "3 hours ago" when date is 3 hours ago from current date`() { + fun `Should return 3 hours ago when date is 3 hours ago from current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.HOUR, -3) }.time whenever(context.getString(R.string.interval_hour_ago)) doReturn "%d hours ago" @@ -68,9 +68,9 @@ class DateExtensionsTest { } @Test - fun `Should return "yesterday" when date is more than 24h from current date`() { + fun `Should return yesterday when date is more than 24h from current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, -1) }.time whenever(context.getString(R.string.interval_yesterday)) doReturn "Yesterday" @@ -81,16 +81,16 @@ class DateExtensionsTest { @Test fun `Should return date format when date is more than 48h from current date`() { - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, -3) }.time assert(date.toDateSpan(context) == dateFormat.format(date)) } @Test - fun `Should return "today" when date is less than 24h from current date`() { + fun `Should return today when date is less than 24h from current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.HOUR, -20) }.time whenever(context.getString(R.string.filter_period_today)) doReturn "Today" @@ -100,7 +100,7 @@ class DateExtensionsTest { @Test fun `Should return dd MMM format when date is same year of current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.MONTH, -2) }.time assert(date.toUiText(context, currentDate) == uiFormat.format(date)) @@ -109,7 +109,7 @@ class DateExtensionsTest { @Test fun `Should return date format format when date is more than one year from current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.YEAR, -2) }.time assert(date.toUiText(context, currentDate) == dateFormat.format(date)) @@ -118,7 +118,7 @@ class DateExtensionsTest { @Test fun `Should return 'Today', when the overdue date is current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.HOUR, -20) }.time whenever(resourceManager.getString(R.string.overdue_today)) doReturn "Today" @@ -128,7 +128,7 @@ class DateExtensionsTest { @Test fun `Should return '1 day overdue', when the overdue date is -1 day from the current date`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, -1) }.time whenever(resourceManager.getPlural(R.plurals.overdue_days, 1, 1)) doReturn "1 day overdue" @@ -138,7 +138,7 @@ class DateExtensionsTest { @Test fun `Should return 'x days overdue', when the overdue date is -x days from the current date and less than 90 days`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, -15) }.time whenever(resourceManager.getPlural(R.plurals.overdue_days, 15, 15)) doReturn "15 days overdue" @@ -148,7 +148,7 @@ class DateExtensionsTest { @Test fun `Should return 'x months overdue', when the overdue date is more than 3 months and less than 1 year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.MONTH, -4) }.time whenever(resourceManager.getPlural(R.plurals.overdue_months, 4, 4)) doReturn "4 months overdue" @@ -158,7 +158,7 @@ class DateExtensionsTest { @Test fun `Should return '1 year overdue', when the overdue date is 1 year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.YEAR, -1) }.time whenever(resourceManager.getPlural(R.plurals.overdue_years, 1, 1)) doReturn "1 year overdue" @@ -168,7 +168,7 @@ class DateExtensionsTest { @Test fun `Should return 'x years overdue', when the overdue date is more than 1 year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.YEAR, -3) }.time whenever(resourceManager.getPlural(R.plurals.overdue_years, 3, 3)) doReturn "3 years overdue" @@ -178,7 +178,7 @@ class DateExtensionsTest { @Test fun `Should return 'In 1 day', when the scheduled date is 1 day`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, 1) }.time whenever(resourceManager.getPlural(R.plurals.schedule_days, 1, 1)) doReturn "In 1 days" @@ -190,7 +190,7 @@ class DateExtensionsTest { val currentDate = currentCalendar().apply { set(Calendar.HOUR_OF_DAY, -2) }.time - val date: Date? = currentCalendar().time + val date = currentCalendar().time whenever(resourceManager.getPlural(R.plurals.schedule_days, 1, 1)) doReturn "In 1 days" assert(date.toOverdueOrScheduledUiText(resourceManager, currentDate) == "In 1 days") } @@ -200,7 +200,7 @@ class DateExtensionsTest { val currentDate = currentCalendar().apply { set(Calendar.HOUR_OF_DAY, 2) }.time - val date: Date? = currentCalendar().time + val date = currentCalendar().time whenever(resourceManager.getString(R.string.overdue_today)) doReturn "Today" assert(date.toOverdueOrScheduledUiText(resourceManager, currentDate) == "Today") } @@ -208,7 +208,7 @@ class DateExtensionsTest { @Test fun `Should return 'In x days', when the scheduled date is same day but different month and same year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.MONTH, 2) }.time whenever(resourceManager.getPlural(R.plurals.schedule_days, 61, 61)) doReturn "In 61 days" @@ -218,7 +218,7 @@ class DateExtensionsTest { @Test fun `Should return 'In x days', when the current date is -x days from the scheduled date and less than 90 days`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.DAY_OF_MONTH, 15) }.time whenever(resourceManager.getPlural(R.plurals.schedule_days, 15, 15)) doReturn "In 15 days" @@ -228,7 +228,7 @@ class DateExtensionsTest { @Test fun `Should return 'In x months', when the current date is more than 3 months and less than 1 year from schedule`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.MONTH, 4) }.time whenever(resourceManager.getPlural(R.plurals.schedule_months, 4, 4)) doReturn "In 4 months" @@ -238,7 +238,7 @@ class DateExtensionsTest { @Test fun `Should return 'In 1 year', when the scheduled date is in 1 year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.YEAR, 1) }.time whenever(resourceManager.getPlural(R.plurals.schedule_years, 1, 1)) doReturn "In 1 year" @@ -248,7 +248,7 @@ class DateExtensionsTest { @Test fun `Should return 'In x years', when the scheduled date is in more than 1 year`() { val currentDate = currentCalendar().time - val date: Date? = currentCalendar().apply { + val date = currentCalendar().apply { add(Calendar.YEAR, 3) }.time whenever(resourceManager.getPlural(R.plurals.schedule_years, 3, 3)) doReturn "In 3 years" diff --git a/app/src/test/java/org/dhis2/bindings/StringExtensionsTest.kt b/app/src/test/java/org/dhis2/bindings/StringExtensionsTest.kt index 0bae5b51a0..dea0bd33e0 100644 --- a/app/src/test/java/org/dhis2/bindings/StringExtensionsTest.kt +++ b/app/src/test/java/org/dhis2/bindings/StringExtensionsTest.kt @@ -99,6 +99,13 @@ class StringExtensionsTest { assert(!new.newVersion(old)) } + @Test + fun `Should return true for new version with lower patch version`() { + val new = "3.1.0" + val old = "3.0.1" + assert(new.newVersion(old)) + } + @Test fun `Should return false when old is bigger`() { val new = "1.2.3" diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventDetailsIntegrationTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventDetailsIntegrationTest.kt index 51d00a38d2..63ae31766e 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventDetailsIntegrationTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventDetailsIntegrationTest.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.periods.domain.GetEventPeriods import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.EventResourcesProvider @@ -19,6 +20,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsViewModel @@ -55,6 +57,7 @@ class EventDetailsIntegrationTest { private val eventResourcesProvider: EventResourcesProvider = mock() private val periodUtils: DhisPeriodUtils = mock() private val preferencesProvider: PreferenceProvider = mock() + private val periodUseCase: GetEventPeriods = mock() // Preconditions, data source private val style: ObjectStyle = mock() @@ -138,6 +141,11 @@ class EventDetailsIntegrationTest { locationProvider = locationProvider, createOrUpdateEventDetails = createOrUpdateEventDetails(), resourcesProvider = provideEventResourcesProvider(), + configurePeriodSelector = ConfigurePeriodSelector( + ENROLLMENT_UID, + eventDetailsRepository, + periodUseCase, + ), ) private fun createConfigureEventCatCombo() = ConfigureEventCatCombo( diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureOrgUnitTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureOrgUnitTest.kt index c9db8514e7..cb12e07fbf 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureOrgUnitTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureOrgUnitTest.kt @@ -3,10 +3,10 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.dhis2.commons.data.EventCreationType +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.prefs.Preference.Companion.CURRENT_ORG_UNIT import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.junit.Before import org.junit.Test diff --git a/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt index 83863fac1c..b0350c42d3 100644 --- a/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/MainPresenterTest.kt @@ -2,13 +2,20 @@ package org.dhis2.usescases.main import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.reactivex.Completable +import io.reactivex.Flowable import io.reactivex.Single +import io.reactivex.processors.BehaviorProcessor +import io.reactivex.processors.FlowableProcessor +import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FilterManager.PeriodRequest +import org.dhis2.commons.filters.Filters +import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.Categories.Companion.HOME import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.Preference.Companion.DEFAULT_CAT_COMBO @@ -53,6 +60,8 @@ class MainPresenterTest { private val preferences: PreferenceProvider = mock() private val workManagerController: WorkManagerController = mock() private val filterManager: FilterManager = mock() + private val filterRepository: FilterRepository = mock() + private val matomoAnalyticsController: MatomoAnalyticsController = mock() private val userManager: UserManager = mock() private val deleteUserData: DeleteUserData = mock() @@ -83,6 +92,7 @@ class MainPresenterTest { preferences, workManagerController, filterManager, + filterRepository, matomoAnalyticsController, userManager, deleteUserData, @@ -103,7 +113,6 @@ class MainPresenterTest { verify(view).renderUsername(any()) verify(preferences).setValue(DEFAULT_CAT_COMBO, "uid") verify(preferences).setValue(PREF_DEFAULT_CAT_OPTION_COMBO, "uid") - verify(filterManager).clearAllFilters() } @Test @@ -151,6 +160,14 @@ class MainPresenterTest { @Test fun `should return to home section when user taps back in a different section`() { + val filterProcessor: FlowableProcessor = PublishProcessor.create() + val filterManagerFlowable = Flowable.just(filterManager).startWith(filterProcessor) + val periodRequest: FlowableProcessor> = + BehaviorProcessor.create() + whenever(filterManager.asFlowable()) doReturn filterManagerFlowable + whenever(filterManager.periodRequest) doReturn periodRequest + whenever(filterManager.ouTreeFlowable()) doReturn Flowable.just(true) + presenter.onNavigateBackToHome() verify(view).goToHome() diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt index e04f232364..4a94d1a716 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt @@ -10,6 +10,7 @@ import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.data.service.SyncStatusData import org.dhis2.ui.MetadataIconData @@ -46,6 +47,7 @@ class ProgramRepositoryImplTest { private val filterPresenter: FilterPresenter = Mockito.mock(FilterPresenter::class.java, Mockito.RETURNS_DEEP_STUBS) private val dhisProgramUtils: DhisProgramUtils = mock() + private val dhis2TeiUtils: DhisTrackedEntityInstanceUtils = mock() private val scheduler = TrampolineSchedulerProvider() private val resourceManager: ResourceManager = mock() private val metadataIconProvider: MetadataIconProvider = mock { @@ -60,6 +62,7 @@ class ProgramRepositoryImplTest { d2, filterPresenter, dhisProgramUtils, + dhis2TeiUtils, resourceManager, metadataIconProvider, scheduler, @@ -129,9 +132,9 @@ class ProgramRepositoryImplTest { fun `Should return list of program ProgramViewModels`() { val syncStatusData = SyncStatusData(true) initWheneverForPrograms() - val testOvserver = programRepository.programModels(syncStatusData).test() + val testObserver = programRepository.programModels(syncStatusData).test() - testOvserver + testObserver .assertNoErrors() .assertValue { it.size == mockedPrograms().size && @@ -175,6 +178,8 @@ class ProgramRepositoryImplTest { Event.builder().uid("9").syncState(State.SYNCED).build(), Event.builder().uid("10").syncState(State.RELATIONSHIP).build(), ) + whenever(dhis2TeiUtils.hasOverdueInProgram(any(), any())) doReturn false + whenever(filterPresenter.areFiltersActive()) doReturn false whenever( filterPresenter.filteredTrackerProgram(any()), ) doReturn mock() @@ -182,8 +187,8 @@ class ProgramRepositoryImplTest { filterPresenter.filteredTrackerProgram(any()).offlineFirst(), ) doReturn mock() whenever( - filterPresenter.filteredTrackerProgram(any()).offlineFirst().blockingCount(), - ) doReturn 2 + filterPresenter.filteredTrackerProgram(any()).offlineFirst().blockingGetUids(), + ) doReturn listOf("0", "1") } private fun mockedDataSetInstanceSummaries(): List { diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt index fe4e7767af..78c88b39ba 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt @@ -2,6 +2,9 @@ package org.dhis2.usescases.main.program import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.reactivex.Flowable +import io.reactivex.processors.FlowableProcessor +import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -9,8 +12,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.schedulers.TestSchedulerProvider import org.dhis2.data.service.SyncStatusController import org.dhis2.data.service.SyncStatusData import org.dhis2.ui.MetadataIconData @@ -43,6 +48,8 @@ class ProgramViewModelTest { private val view: ProgramView = mock() private val programRepository: ProgramRepository = mock() private val matomoAnalyticsController: MatomoAnalyticsController = mock() + private val filterManager: FilterManager = mock() + private val schedulerProvider: TestSchedulerProvider = TestSchedulerProvider(TestScheduler()) private val syncStatusController: SyncStatusController = mock() private val testingDispatcher = UnconfinedTestDispatcher() private val featureConfigRepository: FeatureConfigRepository = mock { @@ -70,8 +77,11 @@ class ProgramViewModelTest { programRepository, featureConfigRepository, dispatcherProvider, + matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) } @@ -79,7 +89,12 @@ class ProgramViewModelTest { fun `Should initialize program list`() { val programs = listOf(programViewModel()) val programsFlowable = Flowable.just(programs) + val filterProcessor: FlowableProcessor = PublishProcessor.create() + val syncStatusData = SyncStatusData(true) + val filterManagerFlowable = Flowable.just(filterManager).startWith(filterProcessor) + + whenever(filterManager.asFlowable()) doReturn filterManagerFlowable whenever( syncStatusController.observeDownloadProcess(), @@ -149,6 +164,8 @@ class ProgramViewModelTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) } @@ -176,6 +193,8 @@ class ProgramViewModelTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) } } diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt index 7dfb99982c..296767dc0d 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryTest.kt @@ -1,25 +1,63 @@ package org.dhis2.usescases.searchTrackEntity +import dhis2.org.analytics.charts.Charts import kotlinx.coroutines.Dispatchers +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.Filters +import org.dhis2.commons.filters.data.FilterPresenter +import org.dhis2.commons.filters.sorting.SortingItem +import org.dhis2.commons.network.NetworkUtils +import org.dhis2.commons.reporting.CrashReportController +import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.forms.dataentry.SearchTEIRepository +import org.dhis2.data.sorting.SearchSortingValueSetter import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.FieldUiModelImpl import org.dhis2.form.model.UiRenderType import org.dhis2.form.ui.FieldViewModelFactory +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.ui.ThemeManager import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.filters.internal.BooleanFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.EnumFilterConnector +import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentCollectionRepository +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +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.organisationunit.OrganisationUnitCollectionRepository +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramCollectionRepository import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeCollectionRepository +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.Calendar +import java.util.Date class SearchRepositoryTest { @@ -30,6 +68,34 @@ class SearchRepositoryTest { on { io() } doReturn Dispatchers.IO } private lateinit var searchRepository: SearchRepositoryImplKt + private lateinit var searchRepositoryJava: SearchRepository + + private val trackedEntitySearchItemHelper: TrackedEntitySearchItemHelper = mock() + + private val enrollmentCollectionRepository: EnrollmentCollectionRepository = mock() + private val stringFilterConnector: StringFilterConnector = mock() + private val booleanFilterConnector: BooleanFilterConnector = mock() + + private val programCollectionRepository: ProgramCollectionRepository = mock() + private val programReadOnlyOneObjectRepository: ReadOnlyOneObjectRepositoryFinalImpl = mock() + + private val eventCollectionRepository: EventCollectionRepository = mock() + private val enumEventFilterConnector: EnumFilterConnector = mock() + private val stringEventFilterConnector: StringFilterConnector = mock() + + private val orgUnitCollectionRepository: OrganisationUnitCollectionRepository = mock() + private val readOnlyOneObjectRepository: ReadOnlyOneObjectRepositoryFinalImpl = mock() + + private val filterPresenter: FilterPresenter = mock() + private val resourceManager: ResourceManager = mock() + private val sortingValueSetter: SearchSortingValueSetter = mock() + private val dhisPeriodUtils: DhisPeriodUtils = mock() + private val charts: Charts = mock() + private val crashReporterController: CrashReportController = mock() + private val networkUtils: NetworkUtils = mock() + private val searchTEIRepository: SearchTEIRepository = mock() + private val themeManager: ThemeManager = mock() + private val profilePictureProvider: ProfilePictureProvider = mock() @Before fun setUp() { @@ -56,6 +122,23 @@ class SearchRepositoryTest { trackedEntityInstanceInfoProvider = mock(), eventInfoProvider = mock(), ) + + searchRepositoryJava = SearchRepositoryImpl( + "teiType", + null, + d2, + filterPresenter, + resourceManager, + sortingValueSetter, + dhisPeriodUtils, + charts, + crashReporterController, + networkUtils, + searchTEIRepository, + themeManager, + metadataIconProvider, + profilePictureProvider, + ) } @Test @@ -75,6 +158,241 @@ class SearchRepositoryTest { assertEquals("state", sortedData[9].uid) } + @Test + fun shouldTransformToSearchTeiModelWithOverdueEvents() { + val searchItem = getTrackedEntitySearchItem("header") + val program = Program.builder().uid("programUid").build() + val sorting = SortingItem.create(Filters.ENROLLMENT_DATE) + val tei = TrackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem) + + val overdueDate = DateUtils.getInstance().getCalendarByDate(Date()) + overdueDate.add(Calendar.DATE, -2) + + val enrollmentsInProgram = listOf( + createEnrollment("enrollmentUid", "orgUnit", program.uid()), + createEnrollment("enrollmentUid_2", "orgUnit", program.uid()), + ) + val allEnrollments = listOf( + createEnrollment("enrollmentUid_3", "orgUnit", "uid"), + createEnrollment("enrollmentUid_4", "orgUnit_2", "uid"), + ) + val events = listOf( + createEvent("eventUid", EventStatus.OVERDUE, overdueDate.time), + createEvent("eventUid", EventStatus.SCHEDULE, Date()), + ) + + mockedSdkCalls(searchItem, tei, enrollmentsInProgram, allEnrollments, events) + + val result = searchRepositoryJava.transform(searchItem, program, true, sorting) + + assertTrue(result.isHasOverdue) + } + + @Test + fun shouldTransformToSearchTeiModelWithOutOverdueEvents() { + val searchItem = getTrackedEntitySearchItem("header") + val program = Program.builder().uid("programUid").build() + val sorting = SortingItem.create(Filters.ENROLLMENT_DATE) + val tei = TrackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem) + + val overdueDate = DateUtils.getInstance().getCalendarByDate(Date()) + overdueDate.add(Calendar.DATE, -2) + + val enrollmentsInProgram = listOf( + createEnrollment("enrollmentUid", "orgUnit", program.uid()), + createEnrollment("enrollmentUid_2", "orgUnit", program.uid()), + ) + val allEnrollments = listOf( + createEnrollment("enrollmentUid_3", "orgUnit", "uid"), + createEnrollment("enrollmentUid_4", "orgUnit_2", "uid"), + ) + val events = listOf( + createEvent("eventUid", EventStatus.SCHEDULE, Date()), + ) + + mockedSdkCalls(searchItem, tei, enrollmentsInProgram, allEnrollments, events) + + val result = searchRepositoryJava.transform(searchItem, program, true, sorting) + + assertFalse(result.isHasOverdue) + } + + private fun mockedSdkCalls( + searchItem: TrackedEntitySearchItem, + teiToReturn: TrackedEntityInstance, + enrollmentsInProgramToReturn: List = listOf(), + enrollmentsForInfoToReturn: List = listOf(), + eventsToReturn: List = listOf(), + profilePathToReturn: String = "", + orgUnitCount: Int = 1, + ) { + whenever( + trackedEntitySearchItemHelper.toTrackedEntityInstance(searchItem), + ) doReturn teiToReturn + + if (searchItem.isOnline) { + whenever(d2.trackedEntityModule().trackedEntityInstances()) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() + .uid(any()), + ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() + .uid(any()) + .blockingGet(), + ) doReturn teiToReturn + } + + whenever(d2.enrollmentModule().enrollments()) doReturn mock() + whenever( + d2.enrollmentModule().enrollments() + .byTrackedEntityInstance(), + ) doReturn mock() + whenever( + d2.enrollmentModule().enrollments() + .byTrackedEntityInstance().eq(any()), + ) doReturn enrollmentCollectionRepository + + whenever( + enrollmentCollectionRepository.byProgram(), + ) doReturn stringFilterConnector + whenever( + stringFilterConnector.eq(any()), + ) doReturn enrollmentCollectionRepository + + whenever( + enrollmentCollectionRepository.byProgram().eq(any()), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.orderByEnrollmentDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.blockingGet(), + ) doReturn enrollmentsInProgramToReturn + + // Mock setEnrollmentInfo + whenever( + enrollmentCollectionRepository.byDeleted(), + ) doReturn booleanFilterConnector + whenever( + booleanFilterConnector.eq(any()), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.byDeleted().eq(false), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.orderByCreated(RepositoryScope.OrderByDirection.DESC), + ) doReturn enrollmentCollectionRepository + whenever( + enrollmentCollectionRepository.blockingGet(), + ) doReturn enrollmentsForInfoToReturn + + val programUid = if (enrollmentsForInfoToReturn.isNotEmpty()) enrollmentsForInfoToReturn[0].program() else "programUid" + whenever(d2.programModule().programs()) doReturn programCollectionRepository + whenever( + programCollectionRepository.uid(any()), + ) doReturn programReadOnlyOneObjectRepository + whenever( + programReadOnlyOneObjectRepository.blockingGet(), + ) doReturn Program.builder().uid(programUid).displayFrontPageList(true).build() + + // Mock setOverdueEvents + whenever(d2.eventModule().events()) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byEnrollmentUid(), + ) doReturn stringEventFilterConnector + whenever( + stringEventFilterConnector.`in`(any>()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byEnrollmentUid().`in`(any>()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byStatus(), + ) doReturn enumEventFilterConnector + whenever( + enumEventFilterConnector.eq(EventStatus.OVERDUE), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byStatus().eq(EventStatus.OVERDUE), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.byProgramUid(), + ) doReturn stringEventFilterConnector + whenever( + eventCollectionRepository.byProgramUid().eq(any()), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.orderByDueDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn eventCollectionRepository + whenever( + eventCollectionRepository.blockingGet(), + ) doReturn eventsToReturn.filter { it.status() == EventStatus.OVERDUE } + + // mock orgUnitName(orgUnitUid) + whenever(d2.organisationUnitModule().organisationUnits()) doReturn orgUnitCollectionRepository + whenever( + orgUnitCollectionRepository.uid(any()), + ) doReturn readOnlyOneObjectRepository + whenever(readOnlyOneObjectRepository.blockingGet()) doReturn OrganisationUnit.builder().uid("uid").displayName("orgUnitName").build() + + whenever(profilePictureProvider.invoke(any(), any())) doReturn profilePathToReturn + + // mock displayOrgUnit() + whenever( + orgUnitCollectionRepository.byProgramUids(any()), + ) doReturn orgUnitCollectionRepository + whenever( + orgUnitCollectionRepository.blockingCount(), + ) doReturn orgUnitCount + } + + private fun getTrackedEntitySearchItem( + header: String?, + isOnline: Boolean = false, + state: State = State.SYNCED, + attributesValues: List = listOf(), + ): TrackedEntitySearchItem { + return TrackedEntitySearchItem( + uid = "uid", + created = Date(), + lastUpdated = Date(), + createdAtClient = Date(), + lastUpdatedAtClient = Date(), + organisationUnit = "orgUnit", + geometry = null, + syncState = state, + aggregatedSyncState = state, + deleted = false, + isOnline = isOnline, + type = TrackedEntityType.builder().uid("uid").build(), + header = header, + attributeValues = attributesValues, + ) + } + + private fun createEnrollment( + uid: String, + orgUnitUid: String, + programUid: String, + status: EnrollmentStatus = EnrollmentStatus.ACTIVE, + ) = + Enrollment.builder() + .uid(uid) + .organisationUnit(orgUnitUid) + .program(programUid) + .status(status) + .build() + + private fun createEvent( + uid: String, + status: EventStatus = EventStatus.ACTIVE, + dueDate: Date = Date(), + ) = Event.builder().uid(uid) + .status(status) + .dueDate(dueDate) + .build() + private fun createTrackedEntityAttributeRepository( uid: String, unique: Boolean, diff --git a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt index 25278c3317..d83c8ea84e 100644 --- a/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt +++ b/app/src/test/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapperTest.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.searchTrackEntity.ui.mapper import android.content.Context import org.dhis2.R +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager @@ -19,13 +20,13 @@ import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import java.util.Calendar import java.util.Date class TEICardMapperTest { private val context: Context = mock() private val resourceManager: ResourceManager = mock() - private val currentDate = Date() private lateinit var mapper: TEICardMapper @@ -49,7 +50,7 @@ class TEICardMapperTest { @Test fun shouldReturnCardFull() { - val model = createFakeModel() + val model = createFakeModel(isOverdue = true) val result = mapper.map( searchTEIModel = model, @@ -81,7 +82,31 @@ class TEICardMapperTest { ) } - private fun createFakeModel(): SearchTeiModel { + @Test + fun shouldShowOverDueLabel() { + val overdueDate = DateUtils.getInstance().calendar + overdueDate.add(Calendar.DATE, -2) + + whenever(resourceManager.getPlural(any(), any(), any())) doReturn "2 days" + + val model = createFakeModel(overdueDate.time, true) + + val result = mapper.map( + searchTEIModel = model, + onSyncIconClick = {}, + onCardClick = {}, + onImageClick = {}, + ) + assertEquals( + result.additionalInfo[4].value, + model.overdueDate.toOverdueOrScheduledUiText(resourceManager), + ) + } + + private fun createFakeModel( + currentDate: Date = Date(), + isOverdue: Boolean = false, + ): SearchTeiModel { val attributeValues = LinkedHashMap() attributeValues["Name"] = TrackedEntityAttributeValue.builder() .value("Peter") @@ -121,7 +146,7 @@ class TEICardMapperTest { null, ) overdueDate = currentDate - isHasOverdue = true + isHasOverdue = isOverdue addEnrollment( Enrollment.builder() diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt index 81f92f4d9f..a561ad4581 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt @@ -289,7 +289,7 @@ class TeiDataPresenterTest { ), ) doReturn (Result.success(eventUid)) - teiDataPresenter.onOrgUnitForNewEventSelected( + teiDataPresenter.onNewEventSelected( orgUnitUid, programStageUid, ) @@ -319,7 +319,7 @@ class TeiDataPresenterTest { whenever(d2ErrorUtils.getErrorMessage(d2Error)) doReturn (errorMessage) - teiDataPresenter.onOrgUnitForNewEventSelected( + teiDataPresenter.onNewEventSelected( orgUnitUid, programStageUid, ) diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenterTest.kt index 3d59c88f4f..970ac07030 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenterTest.kt @@ -9,8 +9,6 @@ import org.dhis2.maps.usecases.MapStyleConfiguration import org.dhis2.tracker.relationships.data.RelationshipsRepository import org.dhis2.tracker.ui.AvatarProvider import org.dhis2.utils.analytics.AnalyticsHelper -import org.dhis2.utils.analytics.CLICK -import org.dhis2.utils.analytics.DELETE_RELATIONSHIP import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.common.State @@ -24,7 +22,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito -import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.times @@ -43,10 +40,6 @@ class RelationshipPresenterTest { private val relationshipMapsRepository: RelationshipMapsRepository = mock() private val analyticsHelper: AnalyticsHelper = mock() private val mapRelationshipsToFeatureCollection: MapRelationshipsToFeatureCollection = mock() - private val relationshipConstrain: RelationshipConstraint = mock() - private val relationshipType: RelationshipType = mock { - on { fromConstraint() } doReturn relationshipConstrain - } private val mapStyleConfiguration: MapStyleConfiguration = mock() private val relationshipsRepository: RelationshipsRepository = mock() private val avatarProvider: AvatarProvider = mock() @@ -85,44 +78,31 @@ class RelationshipPresenterTest { @Test fun `If user has permission should create a new relationship`() { + val relationshipTypeUid = "relationshipTypeUid" + val teiTypeToAdd = "teiTypeToAdd" + whenever( - d2.relationshipModule().relationshipService().hasAccessPermission(relationshipType), + relationshipsRepository.hasWritePermission(relationshipTypeUid), ) doReturn true - presenter.goToAddRelationship("teiType", relationshipType) + presenter.goToAddRelationship(relationshipTypeUid, teiTypeToAdd) - verify(view, times(1)).goToAddRelationship("teiUid", "teiType") + verify(view, times(1)).goToAddRelationship("teiUid", teiTypeToAdd) verify(view, times(0)).showPermissionError() } @Test fun `If user don't have permission should show an error`() { + val relationshipTypeUid = "relationshipTypeUid" whenever( - d2.relationshipModule().relationshipService().hasAccessPermission(relationshipType), + relationshipsRepository.hasWritePermission(relationshipTypeUid), ) doReturn false - presenter.goToAddRelationship("teiType", relationshipType) + presenter.goToAddRelationship(relationshipTypeUid, "teiTypeToAdd") verify(view, times(1)).showPermissionError() } - @Test - fun `Should delete relationship`() { - presenter.deleteRelationship(getMockedRelationship().uid()!!) - verify(analyticsHelper).setEvent(DELETE_RELATIONSHIP, CLICK, DELETE_RELATIONSHIP) - } - - @Test - fun `Should create a relationship`() { - whenever( - d2.relationshipModule().relationshipTypes().withConstraints().uid("relationshipTypeUid") - .blockingGet(), - ) doReturn getMockedRelationshipType(true) - presenter.addRelationship("selectedTei", "relationshipTypeUid") - - verify(view, times(0)).displayMessage(any()) - } - @Test fun `Should open dashboard`() { whenever( @@ -213,7 +193,7 @@ class RelationshipPresenterTest { private fun getMockedTei(state: State): TrackedEntityInstance { return TrackedEntityInstance.builder() .uid("teiUid") - .state(state) + .aggregatedSyncState(state) .build() } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt index 9bec3f4a47..215a4aa8db 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelTest.kt @@ -9,12 +9,14 @@ import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.bindings.programStage import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.viewmodel.DispatcherProvider +import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.DateUtils import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentCollectionRepository import org.hisp.dhis.android.core.enrollment.EnrollmentModule import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository +import org.hisp.dhis.android.core.period.PeriodModule import org.hisp.dhis.android.core.program.ProgramModule import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.program.ProgramStageCollectionRepository @@ -55,15 +57,22 @@ class SchedulingViewModelTest { on { programStages() } doReturn programStageCollectionRepository } + private val periodModule: PeriodModule = mock { + on { periodHelper() } doReturn mock() + } + + private val d2: D2 = mock { + on { enrollmentModule() } doReturn enrollmentModule + on { programModule() } doReturn programModule + on { periodModule() } doReturn periodModule + } + @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { Dispatchers.setMain(testingDispatcher) schedulingViewModel = SchedulingViewModel( - d2 = mock { - on { enrollmentModule() } doReturn enrollmentModule - on { programModule() } doReturn programModule - }, + d2 = d2, resourceManager = mock(), eventResourcesProvider = mock(), periodUtils = mock(), @@ -87,6 +96,7 @@ class SchedulingViewModelTest { showYesNoOptions = false, eventCreationType = EventCreationType.SCHEDULE, ), + getEventPeriods = mock(), ) } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt index 872946e1cb..9b6e696f9d 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt @@ -175,6 +175,8 @@ class TeiProgramListPresenterTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + filtersAreActive = false, + hasOverdueEvent = false, ) } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImplTest.kt index 0757f019bc..838b528564 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImplTest.kt @@ -1,9 +1,9 @@ package org.dhis2.usescases.teiDashboard.teiProgramList import io.reactivex.Single +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.usescases.main.program.ProgramViewModelMapper -import org.dhis2.utils.DateUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentCreateProjection diff --git a/app/src/test/java/org/dhis2/utils/DateUtilsTest.java b/app/src/test/java/org/dhis2/utils/DateUtilsTest.java index 93e861fc07..0ae2ce1101 100644 --- a/app/src/test/java/org/dhis2/utils/DateUtilsTest.java +++ b/app/src/test/java/org/dhis2/utils/DateUtilsTest.java @@ -1,10 +1,16 @@ package org.dhis2.utils; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import org.dhis2.R; import org.dhis2.bindings.StringExtensionsKt; -import org.dhis2.usescases.datasets.datasetInitial.DateRangeInputPeriodModel; -import org.dhis2.utils.DateUtils; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.date.Period; +import org.dhis2.usescases.datasets.datasetInitial.DateRangeInputPeriodModel; import org.hisp.dhis.android.core.event.EventStatus; import org.hisp.dhis.android.core.period.PeriodType; import org.junit.Assert; @@ -16,17 +22,12 @@ import java.util.Date; import java.util.Locale; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; - public class DateUtilsTest { @Test public void expiryPeriodAndDaysInRange() throws ParseException { + Locale.setDefault(new Locale("es", "ES")); + String testDateInRange = "2018-07-31"; Date dateInRange = DateUtils.oldUiDateFormat().parse(testDateInRange); @@ -522,9 +523,9 @@ public void active_event_NcD_NPT_NeD_is_not_expired() throws ParseException { Date currentDate = DateUtils.oldUiDateFormat().parse("2019-03-01"); DateUtils.getInstance().setCurrentDate(currentDate); - assertTrue(!DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.ACTIVE, 0, null, 0)); - assertTrue(!DateUtils.getInstance().isEventExpired(toDate("2019-03-02"), null, EventStatus.ACTIVE, 0, null, 0)); - assertTrue(!DateUtils.getInstance().isEventExpired(toDate("2019-02-28"), null, EventStatus.ACTIVE, 0, null, 0)); + assertFalse(DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.ACTIVE, 0, null, 0)); + assertFalse(DateUtils.getInstance().isEventExpired(toDate("2019-03-02"), null, EventStatus.ACTIVE, 0, null, 0)); + assertFalse(DateUtils.getInstance().isEventExpired(toDate("2019-02-28"), null, EventStatus.ACTIVE, 0, null, 0)); } @@ -564,7 +565,7 @@ public void complete_event_NcD_NPT_NeD_is_not_expired() throws ParseException { Date currentDate = DateUtils.oldUiDateFormat().parse("2019-03-01"); DateUtils.getInstance().setCurrentDate(currentDate); - assertTrue(!DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.COMPLETED, 0, null, 0)); + assertFalse(DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.COMPLETED, 0, null, 0)); } @@ -574,7 +575,7 @@ public void complete_event_1_NPT_NeD_is_not_expired() throws ParseException { Date currentDate = DateUtils.oldUiDateFormat().parse("2019-03-01"); DateUtils.getInstance().setCurrentDate(currentDate); - assertTrue(!DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.COMPLETED, 0, null, 1)); + assertFalse(DateUtils.getInstance().isEventExpired(toDate("2019-03-01"), null, EventStatus.COMPLETED, 0, null, 1)); } @@ -641,7 +642,7 @@ public void isInputPeriodDateInsideFutureOpenDayConfiguration() throws ParseExce DateUtils.oldUiDateFormat().parse("2022-11-05") ); - assertEquals(true, DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod, 5)); + assertTrue(DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod.endPeriodDate(), 5)); } @Test @@ -656,7 +657,7 @@ public void isInputPeriodDateOutsideFutureOpenDayConfiguration() throws ParseExc DateUtils.oldUiDateFormat().parse("2022-11-15") ); - assertEquals(false, DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod, 5)); + assertFalse(DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod.endPeriodDate(), 5)); } @Test @@ -670,11 +671,11 @@ public void isFutureInputPeriodsNotConfigured() { new Date() ); - assertEquals(false, DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod, 0)); + assertFalse(DateUtils.getInstance().isInsideFutureInputPeriod(inputPeriod.endPeriodDate(), 0)); } @Test - public void shouldParseDate(){ + public void shouldParseDate() { String testDate = "2022-01-01'T'12:01:01.001"; Date date = StringExtensionsKt.toDate(testDate); assertNotNull(date); diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index cb40e1fb9e..95de51b8b8 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { api(libs.dhis2.android.sdk) { exclude("org.hisp.dhis", "core-rules") exclude("com.facebook.flipper") + exclude("com.facebook.soloader") this.isChanging = true } @@ -98,7 +99,6 @@ dependencies { api(libs.google.gson) api(libs.dagger) kapt(libs.dagger.compiler) - api(libs.google.material.themeadapter) api(libs.barcodeScanner.zxing) api(libs.rx.java) api(libs.rx.android) diff --git a/commons/src/androidTest/java/org/dhis2/commons/date/DateUtilsTest.java b/commons/src/androidTest/java/org/dhis2/commons/date/DateUtilsTest.java new file mode 100644 index 0000000000..482e330e2b --- /dev/null +++ b/commons/src/androidTest/java/org/dhis2/commons/date/DateUtilsTest.java @@ -0,0 +1,27 @@ +package org.dhis2.commons.date; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import java.util.Calendar; +import java.util.Date; + +public class DateUtilsTest { + + + @Test + public void returnsEventOverDueDateCorrectly() { + + Calendar calendar = Calendar.getInstance(); + //should return false for current date + calendar.setTime(new Date()); + assertFalse(DateUtils.getInstance().isEventDueDateOverdue(calendar.getTime())); + //false for future date + calendar.add(Calendar.DAY_OF_MONTH, 10); + assertFalse(DateUtils.getInstance().isEventDueDateOverdue(calendar.getTime())); + //true for past dates + calendar.add(Calendar.DAY_OF_MONTH, -30); + assertTrue(DateUtils.getInstance().isEventDueDateOverdue(calendar.getTime())); + + } +} diff --git a/commons/src/main/java/org/dhis2/commons/ActivityResultObserver.kt b/commons/src/main/java/org/dhis2/commons/ActivityResultObserver.kt index c9aa3fb309..6b78ef370a 100644 --- a/commons/src/main/java/org/dhis2/commons/ActivityResultObserver.kt +++ b/commons/src/main/java/org/dhis2/commons/ActivityResultObserver.kt @@ -6,7 +6,7 @@ interface ActivityResultObserver { fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) } diff --git a/commons/src/main/java/org/dhis2/commons/bindings/FileExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/FileExtensions.kt index 0fd8441505..affd7afce4 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/FileExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/FileExtensions.kt @@ -15,6 +15,10 @@ import org.apache.commons.io.FileUtils import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import java.io.File +fun isFilePathValid(filePath: String): Boolean { + return filePath.isNotEmpty() && File(filePath).exists() +} + fun resizeToMinimum(minimum: Int? = null, width: Int, height: Int): Pair { val ratio = width.toFloat() / height.toFloat() 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 2d781401f1..e7084c57d0 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.unit.Dp import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.DrawableCompat -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.commons.R import org.dhis2.commons.data.EnrollmentIconData import org.dhis2.commons.databinding.ItemFieldValueBinding @@ -33,6 +32,7 @@ import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import timber.log.Timber import java.util.Date @@ -85,7 +85,7 @@ fun List.getEnrollmentIconsData( fun List.paintAllEnrollmentIcons(parent: ComposeView) { parent.apply { setContent { - MdcTheme { + DHIS2Theme { Row( horizontalArrangement = spacedBy(Dp(4f)), verticalAlignment = Alignment.CenterVertically, diff --git a/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt b/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt index 17bdc0849e..4a78e00275 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/date/DateExtensions.kt @@ -2,6 +2,7 @@ package org.dhis2.commons.date import android.content.Context import androidx.annotation.PluralsRes +import androidx.annotation.StringRes import org.dhis2.commons.R import org.dhis2.commons.resources.ResourceManager import org.joda.time.Days @@ -14,6 +15,7 @@ import org.joda.time.PeriodType import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import org.hisp.dhis.android.core.period.PeriodType as Dhis2PeriodType val defaultCurrentDate: Date get() = Date() @@ -88,6 +90,7 @@ fun Date?.toOverdueOrScheduledUiText( isOverdue, ) } + val currentDay = with(DateUtils.getInstance()) { setCurrentDate(currentDate) calendar.time @@ -112,6 +115,7 @@ fun Date?.toOverdueOrScheduledUiText( resourceManager.getString(R.string.overdue_today) } } + period.years >= 1 -> { getString( resourceManager, @@ -121,6 +125,7 @@ fun Date?.toOverdueOrScheduledUiText( isOverdue, ) } + period.months >= 3 && period.years < 1 -> { getString( resourceManager, @@ -130,6 +135,7 @@ fun Date?.toOverdueOrScheduledUiText( isOverdue, ) } + period.days in 0..89 && period.months in 0..2 -> { val intervalDays = if (this.time > currentDay.time) { Interval(currentDay.time, this.time) @@ -139,6 +145,7 @@ fun Date?.toOverdueOrScheduledUiText( getOverdueDaysString(intervalDays, isOverdue) } + else -> { getOverdueDaysString(period.days, isOverdue) } @@ -161,3 +168,35 @@ private fun getString( fun Date?.toUi(): String? = this?.let { DateUtils.uiDateFormat().format(this) } + +@StringRes +fun Dhis2PeriodType.toUiStringResource() = + when (this) { + Dhis2PeriodType.Weekly, + Dhis2PeriodType.WeeklySaturday, + Dhis2PeriodType.WeeklySunday, + Dhis2PeriodType.WeeklyThursday, + Dhis2PeriodType.WeeklyWednesday, + -> R.string.period_weekly_title + + Dhis2PeriodType.BiWeekly -> R.string.period_biweekly_title + Dhis2PeriodType.Monthly -> R.string.period_monthly_title + Dhis2PeriodType.BiMonthly -> R.string.period_bi_monthly_title + Dhis2PeriodType.Quarterly, + Dhis2PeriodType.QuarterlyNov, + -> R.string.period_quarter_title + + Dhis2PeriodType.SixMonthly, + Dhis2PeriodType.SixMonthlyApril, + Dhis2PeriodType.SixMonthlyNov, + -> R.string.period_six_monthly_title + + Dhis2PeriodType.Yearly -> R.string.period_yearly_title + Dhis2PeriodType.FinancialApril, + Dhis2PeriodType.FinancialJuly, + Dhis2PeriodType.FinancialOct, + Dhis2PeriodType.FinancialNov, + -> R.string.period_financial_year_title + + Dhis2PeriodType.Daily -> R.string.period_daily_title + } diff --git a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index 490f46de22..dbc3b09a67 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java +++ b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java @@ -335,6 +335,19 @@ public Boolean isEventExpired(@Nullable Date currentDate, Date completedDay, int completedDay.getTime() + TimeUnit.DAYS.toMillis(compExpDays) < date.getTime(); } + /** + * Check if an event due date is overdue + * + * @param dueDate the date the event is due + * @return true or false + */ + public Boolean isEventDueDateOverdue(Date dueDate) { + Date currentDate = getStartOfDay(new Date()); + if(dueDate.equals(currentDate)) return false; + return dueDate.before(currentDate); + } + + /** * @param currentDate Date from which calculation will be carried out. Default value is today. * @param expiryDays Number of extra days to add events on previous period @@ -898,4 +911,23 @@ public Boolean isInsideInputPeriod(DataInputPeriod dataInputPeriodModel) { return dataInputPeriodModel.openingDate().getTime() < Calendar.getInstance().getTime().getTime() && Calendar.getInstance().getTime().getTime() < dataInputPeriodModel.closingDate().getTime(); } + + public Boolean isInsideFutureInputPeriod(Date endPeriodDate, Integer futureOpenDays) { + if (futureOpenDays != null && futureOpenDays > 0) { + boolean isInside = false; + + Date today = DateUtils.getInstance().getToday(); + + long diffInMillis = Math.abs(endPeriodDate.getTime() - today.getTime()); + long diffInDays = TimeUnit.DAYS.convert(diffInMillis, TimeUnit.MILLISECONDS); + + + if (diffInDays < futureOpenDays) { + isInside = true; + } + return isInside; + } else { + return false; + } + } } diff --git a/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModelFactory.kt b/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModelFactory.kt index 08d2b85f73..138ae76d4a 100644 --- a/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModelFactory.kt +++ b/commons/src/main/java/org/dhis2/commons/featureconfig/ui/FeatureConfigViewModelFactory.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.featureconfig.data.FeatureConfigRepository -@Suppress("UNCHECKED_CAST") class FeatureConfigViewModelFactory(val repository: FeatureConfigRepository) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { diff --git a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java index db35d904bd..967fc36412 100644 --- a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java +++ b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java @@ -363,7 +363,6 @@ public FlowableProcessor getOuTreeProcessor() { } public Flowable asFlowable() { - this.scope = null; return filterProcessor; } diff --git a/commons/src/main/java/org/dhis2/commons/filters/data/FilterRepository.kt b/commons/src/main/java/org/dhis2/commons/filters/data/FilterRepository.kt index 116b342d41..3b93368bee 100644 --- a/commons/src/main/java/org/dhis2/commons/filters/data/FilterRepository.kt +++ b/commons/src/main/java/org/dhis2/commons/filters/data/FilterRepository.kt @@ -540,7 +540,7 @@ constructor( ProgramType.TRACKER, observableSortingInject, observableOpenFilter, - program.enrollmentDateLabel() ?: resources + program.displayEnrollmentDateLabel() ?: resources .filterEnrollmentDateLabel(program.uid()), ) defaultTrackerFilters[ProgramFilter.ORG_UNIT] = OrgUnitFilter( diff --git a/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListViewModelFactory.kt b/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListViewModelFactory.kt index 6d80aa0554..d26fdea2cf 100644 --- a/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListViewModelFactory.kt +++ b/commons/src/main/java/org/dhis2/commons/filters/workingLists/WorkingListViewModelFactory.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.filters.data.FilterRepository -@Suppress("UNCHECKED_CAST") class WorkingListViewModelFactory( private val programUid: String?, private val filterRepository: FilterRepository, diff --git a/commons/src/main/java/org/dhis2/commons/network/NetworkUtils.kt b/commons/src/main/java/org/dhis2/commons/network/NetworkUtils.kt index 265805ad2c..2f04ac9e7c 100644 --- a/commons/src/main/java/org/dhis2/commons/network/NetworkUtils.kt +++ b/commons/src/main/java/org/dhis2/commons/network/NetworkUtils.kt @@ -12,7 +12,7 @@ class NetworkUtils(val context: Context) { try { val manager = context.getSystemService( Context.CONNECTIVITY_SERVICE, - ) as ConnectivityManager + ) as ConnectivityManager? if (manager != null) { val netInfo = manager.activeNetworkInfo isOnline = netInfo != null && netInfo.isConnectedOrConnecting diff --git a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt index 4b2758497e..28cddb3b46 100644 --- a/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt +++ b/commons/src/main/java/org/dhis2/commons/orgunitselector/OUTreeViewModel.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.dhis2.commons.idlingresource.CountingIdlingResourceSingleton import org.dhis2.commons.schedulers.SingleEventEnforcer import org.dhis2.commons.schedulers.get import org.dhis2.commons.viewmodel.DispatcherProvider @@ -31,6 +32,7 @@ class OUTreeViewModel( } private fun fetchInitialOrgUnits(name: String? = null) { + CountingIdlingResourceSingleton.increment() viewModelScope.launch(dispatchers.io()) { val orgUnits = repository.orgUnits(name) val treeNodes = ArrayList() @@ -53,6 +55,7 @@ class OUTreeViewModel( ), ) } + CountingIdlingResourceSingleton.decrement() _treeNodes.update { treeNodes } } } @@ -101,6 +104,7 @@ class OUTreeViewModel( } fun onOrgUnitCheckChanged(orgUnitUid: String, isChecked: Boolean) { + CountingIdlingResourceSingleton.increment() viewModelScope.launch(dispatchers.io()) { if (singleSelection) { selectedOrgUnits.clear() @@ -119,11 +123,13 @@ class OUTreeViewModel( ), ) } + CountingIdlingResourceSingleton.decrement() _treeNodes.update { treeNodeList } } } fun clearAll() { + CountingIdlingResourceSingleton.increment() viewModelScope.launch(dispatchers.io()) { selectedOrgUnits.clear() val treeNodeList = treeNodes.value.map { currentTreeNode -> @@ -132,6 +138,7 @@ class OUTreeViewModel( selectedChildrenCount = 0, ) } + CountingIdlingResourceSingleton.decrement() _treeNodes.update { treeNodeList } } } diff --git a/commons/src/main/java/org/dhis2/commons/periods/data/EventPeriodRepository.kt b/commons/src/main/java/org/dhis2/commons/periods/data/EventPeriodRepository.kt new file mode 100644 index 0000000000..875b9c7ab6 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/data/EventPeriodRepository.kt @@ -0,0 +1,102 @@ +package org.dhis2.commons.periods.data + +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.eventsBy +import org.dhis2.commons.bindings.program +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Date + +class EventPeriodRepository(private val d2: D2) { + + fun getEventPeriodMinDate( + programStage: ProgramStage, + isScheduling: Boolean, + eventEnrollmentUid: String?, + ): Date { + val periodType = programStage.periodType() ?: PeriodType.Daily + + val program = programStage.program()?.let { d2.program(it.uid()) } + + val expiryDays = program?.expiryDays() + + val currentDate = if (!isScheduling && eventEnrollmentUid != null) { + val enrollment = d2.enrollment(eventEnrollmentUid) + if (programStage.generatedByEnrollmentDate() == true) { + enrollment?.enrollmentDate() + } else { + enrollment?.incidentDate() ?: enrollment?.enrollmentDate() + } + } else { + generatePeriod(periodType, offset = 1).startDate() + } ?: Date() + + val currentPeriod = generatePeriod(periodType, currentDate) + val previousPeriodLastDay = + generatePeriod(PeriodType.Daily, currentPeriod.startDate()!!, expiryDays ?: 0) + .startDate() + + return if (currentDate.after(previousPeriodLastDay) or (currentDate == previousPeriodLastDay)) { + currentPeriod.startDate() + } else { + generatePeriod(periodType, currentDate, offset = -1).startDate() + } ?: Date() + } + + fun getEventPeriodMaxDate( + programStage: ProgramStage, + isScheduling: Boolean, + eventEnrollmentUid: String?, + ): Date? { + if (isScheduling) return null + + val periodType = programStage.periodType() ?: PeriodType.Daily + + val program = programStage.program()?.let { d2.program(it.uid()) } + + val expiryDays = program?.expiryDays() + + val currentDate = if (expiryDays == null) { + val enrollment = eventEnrollmentUid?.let { d2.enrollment(it) } + if (programStage.generatedByEnrollmentDate() == true) { + enrollment?.enrollmentDate() + } else { + enrollment?.incidentDate() ?: enrollment?.enrollmentDate() + } + } else { + Date() + } ?: Date() + + val currentPeriod = generatePeriod(periodType, currentDate) + + return currentPeriod.startDate() + } + + fun getEventUnavailableDates( + programStageUid: String, + enrollmentUid: String?, + currentEventUid: String?, + ): List { + val enrollment = enrollmentUid?.let { d2.enrollment(it) } + return d2.eventsBy(enrollmentUid = enrollment?.uid()).mapNotNull { + if (it.programStage() == programStageUid && + (currentEventUid == null || it.uid() != currentEventUid) && + it.status() != EventStatus.SKIPPED && + it.deleted() == false + ) { + it.eventDate() ?: it.dueDate() + } else { + null + } + } + } + + fun generatePeriod( + periodType: PeriodType, + date: Date = Date(), + offset: Int = 0, + ) = d2.periodModule().periodHelper() + .blockingGetPeriodForPeriodTypeAndDate(periodType, date, offset) +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/data/PeriodLabelProvider.kt b/commons/src/main/java/org/dhis2/commons/periods/data/PeriodLabelProvider.kt new file mode 100644 index 0000000000..e75f4495dd --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/data/PeriodLabelProvider.kt @@ -0,0 +1,135 @@ +package org.dhis2.commons.periods.data + +import org.apache.commons.text.WordUtils +import org.dhis2.commons.periods.model.DAILY_FORMAT +import org.dhis2.commons.periods.model.FROM_TO_LABEL +import org.dhis2.commons.periods.model.MONTH_DAY_SHORT_FORMAT +import org.dhis2.commons.periods.model.MONTH_FULL_FORMAT +import org.dhis2.commons.periods.model.MONTH_YEAR_FULL_FORMAT +import org.dhis2.commons.periods.model.YEARLY_FORMAT +import org.hisp.dhis.android.core.period.PeriodType +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.regex.Pattern + +class PeriodLabelProvider( + private val defaultQuarterlyLabel: String = "Q%d %s (%s - %s)", + private val defaultWeeklyLabel: String = "Week %d: %s - %s, %s", + private val defaultBiWeeklyLabel: String = "Period %d: %s - %s", +) { + operator fun invoke( + periodType: PeriodType?, + periodId: String, + periodStartDate: Date, + periodEndDate: Date, + locale: Locale, + ): String { + val formattedDate: String + when (periodType) { + PeriodType.Weekly, + PeriodType.WeeklyWednesday, + PeriodType.WeeklyThursday, + PeriodType.WeeklySaturday, + PeriodType.WeeklySunday, + -> { + formattedDate = defaultWeeklyLabel.format( + weekOfTheYear(periodType, periodId), + SimpleDateFormat(MONTH_DAY_SHORT_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_DAY_SHORT_FORMAT, locale).format(periodEndDate), + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.BiWeekly -> { + formattedDate = defaultBiWeeklyLabel.format( + weekOfTheYear(periodType, periodId), + SimpleDateFormat(DAILY_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(DAILY_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.Monthly -> + formattedDate = + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate) + + PeriodType.BiMonthly -> + formattedDate = FROM_TO_LABEL.format( + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate), + ) + + PeriodType.Quarterly, + PeriodType.QuarterlyNov, + -> { + val startYear = SimpleDateFormat(YEARLY_FORMAT, locale).format(periodStartDate) + val endYear = SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate) + val (yearFormat, initMonthFormat) = if (startYear != endYear) { + Pair( + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate), + SimpleDateFormat( + MONTH_YEAR_FULL_FORMAT, + locale, + ).format(periodStartDate), + ) + } else { + Pair( + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodStartDate), + ) + } + formattedDate = defaultQuarterlyLabel.format( + quarter(periodType, periodId), + yearFormat, + initMonthFormat, + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.SixMonthly, + PeriodType.SixMonthlyApril, + PeriodType.FinancialApril, + PeriodType.FinancialJuly, + PeriodType.FinancialOct, + -> + formattedDate = FROM_TO_LABEL.format( + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodEndDate), + ) + + PeriodType.Yearly -> + formattedDate = + SimpleDateFormat( + YEARLY_FORMAT, + locale, + ).format(periodStartDate) + + else -> + formattedDate = + SimpleDateFormat(DAILY_FORMAT, locale).format(periodStartDate) + } + return WordUtils.capitalize(formattedDate) + } + + private fun weekOfTheYear(periodType: PeriodType, periodId: String): Int { + val pattern = + Pattern.compile(periodType.pattern) + val matcher = pattern.matcher(periodId) + var weekNumber = 0 + if (matcher.find()) { + weekNumber = matcher.group(2)?.toInt() ?: 0 + } + return weekNumber + } + + private fun quarter(periodType: PeriodType, periodId: String): Int { + val pattern = + Pattern.compile(periodType.pattern) + val matcher = pattern.matcher(periodId) + var quarterNumber = 0 + if (matcher.find()) { + quarterNumber = matcher.group(2)?.toInt() ?: 0 + } + return quarterNumber + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/data/PeriodSource.kt b/commons/src/main/java/org/dhis2/commons/periods/data/PeriodSource.kt new file mode 100644 index 0000000000..8f7dc3ed3d --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/data/PeriodSource.kt @@ -0,0 +1,71 @@ +package org.dhis2.commons.periods.data + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.dhis2.commons.periods.model.Period +import org.hisp.dhis.android.core.period.PeriodType +import java.util.Date +import java.util.Locale + +internal class PeriodSource( + private val eventPeriodRepository: EventPeriodRepository, + private val periodLabelProvider: PeriodLabelProvider, + private val selectedDate: Date?, + private val periodType: PeriodType, + private val initialDate: Date, + private val maxDate: Date?, +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + return try { + var maxPageReached = false + val periodsPerPage = params.loadSize + val page = params.key ?: 1 + val periods: List = buildList { + repeat(periodsPerPage) { indexInPage -> + val period = eventPeriodRepository.generatePeriod( + periodType, + initialDate, + indexInPage + periodsPerPage * (page - 1), + ) + if (maxDate == null || period.startDate() + ?.before(maxDate) == true || period.startDate() == maxDate + ) { + add( + Period( + id = period.periodId()!!, + name = periodLabelProvider( + periodType = periodType, + periodId = period.periodId()!!, + periodStartDate = period.startDate()!!, + periodEndDate = period.endDate()!!, + locale = Locale.getDefault(), + ), + startDate = period.startDate()!!, + enabled = true, + selected = period.startDate() == selectedDate, + ), + ) + } else { + maxPageReached = true + } + } + } + + LoadResult.Page( + data = periods, + prevKey = if (page == 1) null else (page - 1), + nextKey = if (maxPageReached) null else (page + 1), + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) + ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/domain/GetEventPeriods.kt b/commons/src/main/java/org/dhis2/commons/periods/domain/GetEventPeriods.kt new file mode 100644 index 0000000000..3c477aadab --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/domain/GetEventPeriods.kt @@ -0,0 +1,60 @@ +package org.dhis2.commons.periods.domain + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.dhis2.commons.periods.data.EventPeriodRepository +import org.dhis2.commons.periods.data.PeriodLabelProvider +import org.dhis2.commons.periods.data.PeriodSource +import org.dhis2.commons.periods.model.Period +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Date + +class GetEventPeriods( + private val eventPeriodRepository: EventPeriodRepository, + private val periodLabelProvider: PeriodLabelProvider = PeriodLabelProvider(), +) { + operator fun invoke( + eventUid: String?, + periodType: PeriodType, + selectedDate: Date?, + programStage: ProgramStage, + isScheduling: Boolean, + eventEnrollmentUid: String?, + ): Flow> = Pager( + config = PagingConfig(pageSize = 20, maxSize = 100, initialLoadSize = 20), + pagingSourceFactory = { + PeriodSource( + eventPeriodRepository = eventPeriodRepository, + periodLabelProvider = periodLabelProvider, + periodType = periodType, + initialDate = eventPeriodRepository.getEventPeriodMinDate( + programStage, + isScheduling, + eventEnrollmentUid, + ), + maxDate = eventPeriodRepository.getEventPeriodMaxDate( + programStage, + isScheduling, + eventEnrollmentUid, + ), + selectedDate = selectedDate, + ) + }, + ).flow + .map { paging -> + paging.map { period -> + period.copy( + enabled = eventPeriodRepository.getEventUnavailableDates( + programStageUid = programStage.uid(), + enrollmentUid = eventEnrollmentUid, + currentEventUid = eventUid, + ).contains(period.startDate).not(), + ) + } + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/model/Period.kt b/commons/src/main/java/org/dhis2/commons/periods/model/Period.kt new file mode 100644 index 0000000000..bc8df00de6 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/model/Period.kt @@ -0,0 +1,11 @@ +package org.dhis2.commons.periods.model + +import java.util.Date + +data class Period( + val id: String, + val name: String, + val startDate: Date, + val enabled: Boolean, + val selected: Boolean, +) diff --git a/commons/src/main/java/org/dhis2/commons/periods/model/PeriodConstants.kt b/commons/src/main/java/org/dhis2/commons/periods/model/PeriodConstants.kt new file mode 100644 index 0000000000..02624658cc --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/model/PeriodConstants.kt @@ -0,0 +1,8 @@ +package org.dhis2.commons.periods.model + +internal const val MONTH_FULL_FORMAT = "MMMM" +internal const val MONTH_DAY_SHORT_FORMAT = "MMM d" +internal const val MONTH_YEAR_FULL_FORMAT = "MMMM yyyy" +internal const val YEARLY_FORMAT = "yyyy" +internal const val DAILY_FORMAT = "dd/MM/yyyy" +internal const val FROM_TO_LABEL = "%s - %s" diff --git a/commons/src/main/java/org/dhis2/commons/periods/ui/PeriodSelector.kt b/commons/src/main/java/org/dhis2/commons/periods/ui/PeriodSelector.kt new file mode 100644 index 0000000000..71b64a3c79 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/ui/PeriodSelector.kt @@ -0,0 +1,126 @@ +package org.dhis2.commons.periods.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import org.dhis2.commons.R +import org.dhis2.commons.periods.model.Period +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing8 +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import java.util.Date + +@Composable +fun PeriodSelectorContent( + periods: LazyPagingItems, + scrollState: LazyListState, + onPeriodSelected: (Date) -> Unit, +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + state = scrollState, + ) { + when (periods.loadState.refresh) { + is LoadState.Error -> periods.retry() + LoadState.Loading -> + item { ProgressItem(contentPadding = PaddingValues(Spacing8)) } + + is LoadState.NotLoading -> + if (periods.itemCount == 0) { + item { + ListItem( + contentPadding = PaddingValues(Spacing8), + label = stringResource(R.string.no_periods), + selected = false, + enabled = false, + onItemClick = {}, + ) + } + } else { + items(periods.itemCount) { index -> + val period = periods[index] + ListItem( + contentPadding = PaddingValues(Spacing8), + label = period?.name ?: "", + selected = period?.selected == true, + enabled = period?.enabled == true, + ) { + period?.startDate?.let(onPeriodSelected) + } + } + } + } + } +} + +@Deprecated("Expose design system item", replaceWith = ReplaceWith("DropDownItem")) +@Composable +fun ListItem( + modifier: Modifier = Modifier, + contentPadding: PaddingValues, + label: String, + selected: Boolean, + enabled: Boolean, + onItemClick: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Spacing8)) + .clickable(enabled = enabled, onClick = onItemClick) + .background( + color = if (selected) { + SurfaceColor.PrimaryContainer + } else { + Color.Unspecified + }, + ) + .padding(contentPadding), + ) { + Text( + text = label, + style = if (selected) { + MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) + } else { + MaterialTheme.typography.bodyLarge + }, + color = if (enabled) TextColor.OnSurface else TextColor.OnDisabledSurface, + ) + } +} + +@Composable +fun ProgressItem( + modifier: Modifier = Modifier, + contentPadding: PaddingValues, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Spacing8)) + .background(color = Color.Unspecified) + .padding(contentPadding), + contentAlignment = Alignment.Center, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) + } +} diff --git a/commons/src/main/java/org/dhis2/commons/resources/ResourceManager.kt b/commons/src/main/java/org/dhis2/commons/resources/ResourceManager.kt index d182cabcab..93b9a64f8e 100644 --- a/commons/src/main/java/org/dhis2/commons/resources/ResourceManager.kt +++ b/commons/src/main/java/org/dhis2/commons/resources/ResourceManager.kt @@ -67,7 +67,7 @@ class ResourceManager( ): String { val enrollmentLabel = try { D2Manager.getD2().programModule().programs().uid(programUid).blockingGet() - ?.enrollmentLabel() + ?.displayEnrollmentLabel() } catch (e: Exception) { null } ?: getPlural(R.plurals.enrollment, quantity) diff --git a/commons/src/main/java/org/dhis2/commons/ui/extensions/EdgeToEdgeExtension.kt b/commons/src/main/java/org/dhis2/commons/ui/extensions/EdgeToEdgeExtension.kt new file mode 100644 index 0000000000..d78b03905a --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/ui/extensions/EdgeToEdgeExtension.kt @@ -0,0 +1,53 @@ +package org.dhis2.commons.ui.extensions + +import android.app.Activity +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.util.TypedValue +import android.view.View +import androidx.compose.ui.graphics.toArgb +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import org.dhis2.commons.R +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +fun Activity.handleInsets() { + val rootView: View = window.decorView.findViewById(android.R.id.content) + rootView.background = this.getBackgroundGradientDrawable() + + ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets -> + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout(), + ) + v.updatePadding( + left = bars.left, + top = bars.top, + right = bars.right, + bottom = bars.bottom, + ) + WindowInsetsCompat.CONSUMED + } +} + +private fun Activity.getBackgroundGradientDrawable(): GradientDrawable { + val primary = getColorPrimary() + val white = SurfaceColor.SurfaceBright.toArgb() + return GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf( + primary, + primary, + white, + white, + ), + ) +} + +private fun Context.getColorPrimary(): Int { + val typedValue = TypedValue() + this.theme.resolveAttribute(R.attr.colorPrimary, typedValue, true) + return ContextCompat.getColor(this, typedValue.resourceId) +} diff --git a/commons/src/main/res/values-pt/strings.xml b/commons/src/main/res/values-pt/strings.xml index 8f9aad49e1..e846d1f7ab 100644 --- a/commons/src/main/res/values-pt/strings.xml +++ b/commons/src/main/res/values-pt/strings.xml @@ -1,6 +1,8 @@ + Evento Não ha dados + Configuração de recursos Alterar visualização da agenda claro Aceitar @@ -15,6 +17,7 @@ Google Analytics Tarefas Programas + Eventos Relacionamentos Notas Entrada de dados @@ -22,6 +25,8 @@ Detalhes Selecionar opção Pesquisar + Problema Enviado + Erro ao enviar Problema eventos Agregação de Dados TEI @@ -42,6 +47,8 @@ Data de inscrição Estado do evento Status da inscrição + Acompanhado%s + Sincronizar Para enviar @@ -49,7 +56,9 @@ Suprimir Erro Advertência + A carregar Relação + Enviado por SMS Sincronizado por SMS @@ -66,6 +75,8 @@ Cancelado Este registro está marcado para exclusão. Você não pode acessá-lo. + Inscrição de Unidade organizacional + Hoje Ontém @@ -79,19 +90,34 @@ De - Para Outro A qualquer momento + Lista de trabalho %s + Últimos %d dias Este ano Este bimensal Ultimo bimensal Este Trimestre Último trimestre + Estes seis meses + Últimos seis meses + Últimos %d anos + últimos %d meses + últimas %d semanas Semanas deste ano Meses deste ano Bimensais deste ano Trimestres deste ano Meses do ano passado + Trimestres dos últimos anos Último ano + Estes quinze dias + Últimos quinze dias + Últimos %d dois meses + Últimos %d trimestres + últimos %d meses Ano financeiro Último ano fiscal + Últimos %d anos financeiros + Últimas %d quinzenas Período Diário Semanal @@ -114,13 +140,104 @@ Retroinformacão Indicadores Gráficos + Gráficos e indicadores + + + Inscrito em: + + + A palavra-passe está em falta. Introduza a palavra-passe. + O nome de utilizador está em falta. Por favor, introduza o nome de utilizador. Este servidor está executando uma versão dhis diferente de 2.29. Atualize a versão do servidor ou use o ambiente de teste para continuar. + Este servidor está a executar uma versão do DHIS2 diferente de %s. Actualize a versão do seu servidor ou contacte o seu administrador. + Algo correu mal. Verifique se o URL do servidor, o nome de utilizador e a palavra-passe estão correctos e tente novamente. + Desculpe! Algo falhou no lado do servidor. + O URL fornecido não é um servidor DHIS2. + O nome de utilizador ou a palavra-passe estão incorrectos + Servidor desconhecido Erro ao analisar resultados + Algo inesperado correu mal. + O seu utilizador tem mais do que uma unidade organizacional de raiz. Reveja a sua configuração ou contacte o seu administrador. + Os resultados online excedem o número máximo de instâncias de rastreador que podem ser mostradas. Por favor, refine a sua pesquisa. + O servidor está um pouco ocupado neste momento. Por favor, tente mais tarde. + O utilizador já tem sessão iniciada. + O pedido já foi executado. + O pedido do servidor é inválido. + O nome da aplicação não foi definido. + A versão da aplicação não foi definida. + Não é possível aceder ao armazenamento de chaves do dispositivo. + Criação abortada. O objeto já existe. + Eliminação abortada. O objeto não existe. + Não foi possível instanciar o keystore. + Não foi possível reservar valores para este registo. Por favor, contacte o seu administrador. + Inicie sessão para exportar a base de dados. + A exportação de uma base de dados encriptada não é suportada. + Está a importar uma base de dados já existente. + Termine a sessão para importar uma nova base de dados. + A versão da base de dados importada é superior à suportada. + O ficheiro não foi encontrado. + Ocorreu um erro ao redimensionar a imagem. + Não é possível gerar as coordenadas. + Relatório de trabalho não disponível. + Poderá estar a ficar com poucos valores disponíveis. + Não há nenhum utilizador autenticado. + Não existe nenhum utilizador autenticado offline. + Não existem valores suficientes para reservar no servidor. Por favor contacte o seu administrador. + Está a tentar iniciar sessão offline com um utilizador diferente. + O valor da geometria é inválido. + Não existem valores reservados disponíveis. Vá às definições e volte a preencher os valores quando tiver ligação à Internet. + Não foi possível atualizar o objeto. + O objeto não pôde ser inserido. + Não tem acesso. + O URL do servidor está em falta. Por favor, introduza o URL. + O URL do servidor não está bem formado. Por favor, verifique-o. + Esta versão da aplicação Web de definições não é suportada. + A aplicação Web de definições não está instalada no servidor. + O servidor demorou demasiado tempo para responder. Cancelamos o pedido. + O relacionamento não pode ser actualizado. + Existem muitos períodos. + Não foi possível encontrar a URL. Por favor, verifique-o. + A sua conta de utilizador foi desactivada. Se isto for um erro, contacte o seu administrador. + A sua conta de utilizador está bloqueada. Se isto for um erro, contacte o seu administrador. + O valor não pôde ser definido. + O pedido de reserva de valores demorou demasiado tempo. Por favor, tente novamente. + Existe um problema com os certificados do servidor. Por favor, contacte o seu administrador. + SMS não é compatível. + Não existem atributos suficientes para realizar a pesquisa. Por favor, refine a sua pesquisa com mais atributos. + A pesquisa online está apenas disponível para as unidades organizacionais de pesquisa de utilizadores. + Existem caracteres inválidos na consulta. Por favor, peça ao administrador para verificar a configuração do servidor (relaxedQueryChars,...). + O servidor está indisponível do momento. Tente novamente mais tarde. + Está off-line. Ligue-se à Internet e tente novamente. + Este campo não está disponível para captura móvel Adicionar imagem + Adicionar assinatura + Adicionar ficheiro Esta bem A localização do seu dispositivo está desconectada. Por favor ative-o para capturar coordenadas Continue editando + Agora não + Revisão Completo Valor Descartar mudanças - + Dados + Ligação a rede indisponível + É necessária uma ligação à Internet para utilizar mapas. + + Hoje + + %d dia de atraso + %d dias de atraso + %d dias em atraso + + + %d mês em atraso + %d meses de atraso + %d meses em atraso + + + %d ano de atraso + %d anos de atraso + %d anos em atraso + + diff --git a/commons/src/main/res/values-vi/strings.xml b/commons/src/main/res/values-vi/strings.xml index 9f295b5b2b..bf56788e3d 100644 --- a/commons/src/main/res/values-vi/strings.xml +++ b/commons/src/main/res/values-vi/strings.xml @@ -1,8 +1,9 @@ + Sự Kiện Không có dữ liệu Cấu hình chức năng - Thay đổi xem lịch + Thay đổi lịch xóa Chấp nhận Tiếp tục @@ -16,6 +17,7 @@ Phân tích Nhiệm vụ Chương trình + Sự kiện Mối quan hệ Ghi Chú Nhập dữ liệu @@ -23,6 +25,8 @@ Chi tiết Chọn Tìm kiếm + Vấn đề đã được gửi + Có lỗi khi gửi vấn đề sự kiện Biểu nhập Đối Tượng Theo Dõi @@ -71,6 +75,8 @@ Đã hủy Dữ liệu này được chọn để xóa. Bạn không thể truy cập nó. + Đơn vị đăng ký + Hôm nay Hôm qua @@ -200,8 +206,12 @@ Không có đủ yếu tố để thực hiện tìm kiếm. Vui lòng kiểm tra với nhiều yếu tố hơn. Tìm kiếm trực tuyến chỉ hiển thị trong các đơn vị mà tài khoảng của bạn được cấp. Có các ký tự không hợp lệ trong yêu cầu. Vui lòng liên hệ quản lý để xác nhận cấu hình máy chủ (relaxQueryChars,...) + Không thể truy cập máy chủ. Xin vui lòng thử lại sau. + Bạn đang ngoại tuyến. Vui lòng kết nối mạng và thử lại Trường này không hiển thị để nhập dữ liệu bằng điện thoại Thêm hình ảnh + Thêm chữ ký + Thêm tập tin Ok Tính năng chia sẻ vị trí thiết bị của bạn bị tắt. Vui lòng mở nó để thu thập tọa độ. Tiếp tục chỉnh sửa @@ -210,4 +220,18 @@ Hoàn tất Giá trị Hủy bỏ các thay đổi - + Dữ liệu + Không có kết nối mạng + Cần có kết nối mạng để sử dụng bản đồ + + Hôm nay + + Trễ hẹn %dngày + + + Trễ hẹn %d tháng + + + Trễ hẹn %d năm + + diff --git a/commons/src/main/res/values/strings.xml b/commons/src/main/res/values/strings.xml index b00ca0e018..232152a4dd 100644 --- a/commons/src/main/res/values/strings.xml +++ b/commons/src/main/res/values/strings.xml @@ -304,8 +304,19 @@ There are no %s registered, click "+" to add a new one. There are no %s registered. There are planned %s that won\'t be re-scheduled + re-open Retry sync Synchronizing... Remove + Daily + Week + Biweekly + Month + Bimonthly + Quarter + Six monthly + Year + Financial year + No periods available\n diff --git a/compose-table/build.gradle.kts b/compose-table/build.gradle.kts index 0d8159c902..af83802cbc 100644 --- a/compose-table/build.gradle.kts +++ b/compose-table/build.gradle.kts @@ -55,4 +55,5 @@ dependencies { testImplementation(libs.bundles.table.test) androidTestImplementation(libs.bundles.table.androidTest) implementation(libs.dhis2.mobile.designsystem) + implementation(libs.androidx.material3) } diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/ColumnTableTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/ColumnTableTest.kt index 189cec100c..904efcda39 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/ColumnTableTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/ColumnTableTest.kt @@ -1,9 +1,9 @@ package org.dhis2.composetable -import androidx.compose.material.lightColors import androidx.compose.ui.test.junit4.createComposeRule import org.dhis2.composetable.model.FakeModelType import org.dhis2.composetable.ui.TableColors +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.junit.Rule import org.junit.Test @@ -34,7 +34,7 @@ class ColumnTableTest { val firstTableId = fakeModel[0].id!! clickOnHeaderElement(firstTableId, 2, 3) - assertColumnHeaderBackgroundColor(firstTableId, 2, 3, lightColors().primary) + assertColumnHeaderBackgroundColor(firstTableId, 2, 3, SurfaceColor.Primary) } } diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/RowTableTest.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/RowTableTest.kt index edb1253dca..d345cf4feb 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/RowTableTest.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/RowTableTest.kt @@ -1,12 +1,9 @@ package org.dhis2.composetable -import androidx.compose.material.lightColors -import androidx.compose.ui.graphics.Color import androidx.compose.ui.test.junit4.createComposeRule import org.dhis2.composetable.model.FakeModelType -import org.dhis2.composetable.model.FakeTableModels import org.dhis2.composetable.ui.TableColors -import org.junit.Before +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.junit.Rule import org.junit.Test @@ -16,8 +13,6 @@ class RowTableTest { val composeTestRule = createComposeRule() private val tableColors = TableColors() - var primaryColor: Color = lightColors().primary - @Test fun shouldClickOnFirstRowElementAndHighlightAllElements() { tableRobot(composeTestRule) { @@ -29,7 +24,7 @@ class RowTableTest { assertRowHeaderBackgroundChangeToPrimary( firstTableId, 0, - primaryColor.let { tableColors.copy(primary = it) } ?: tableColors + tableColors.copy(primary = SurfaceColor.Primary) ) } } diff --git a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt index 06424d9d2e..77f29615ae 100644 --- a/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt +++ b/compose-table/src/androidTest/java/org/dhis2/composetable/TableRobot.kt @@ -1,7 +1,7 @@ package org.dhis2.composetable import androidx.annotation.DrawableRes -import androidx.compose.material.MaterialTheme +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -73,6 +73,8 @@ import org.dhis2.composetable.ui.semantics.RowIndexHeader import org.dhis2.composetable.ui.semantics.TableId import org.dhis2.composetable.ui.semantics.TableIdColumnHeader import org.dhis2.composetable.utils.KeyboardHelper +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import org.junit.Assert fun tableRobot( @@ -103,7 +105,7 @@ class TableRobot( mutableStateOf(TableSelection.Unselected()) } TableTheme( - tableColors = TableColors().copy(primary = MaterialTheme.colors.primary), + tableColors = tableColors.copy(primary = SurfaceColor.Primary), tableConfiguration = TableConfiguration(headerActionsEnabled = false), tableResizeActions = object : TableResizeActions {} ) { @@ -140,7 +142,7 @@ class TableRobot( keyboardHelper.view = LocalView.current var model by remember { mutableStateOf(screenState) } TableTheme( - tableColors = TableColors().copy(primary = MaterialTheme.colors.primary), + tableColors = TableColors().copy(primary = SurfaceColor.Primary), tableConfiguration = tableConfiguration, tableResizeActions = object : TableResizeActions {} ) { @@ -187,7 +189,7 @@ class TableRobot( val model by remember { mutableStateOf(screenState) } TableTheme( - tableColors = TableColors().copy(primary = MaterialTheme.colors.primary), + tableColors = TableColors().copy(primary = MaterialTheme.colorScheme.primary), tableConfiguration = TableConfiguration(), tableResizeActions = object : TableResizeActions {} ) { @@ -277,7 +279,7 @@ class TableRobot( composeTestRule.onNode( SemanticsMatcher.expectValue(TableId, tableId) .and(SemanticsMatcher.expectValue(RowIndex, rowIndex)) - .and(SemanticsMatcher.expectValue(RowBackground, tableColors.primary)) + .and(SemanticsMatcher.expectValue(RowBackground, SurfaceColor.Primary)) ).assertExists() } diff --git a/compose-table/src/main/java/org/dhis2/composetable/model/TableModel.kt b/compose-table/src/main/java/org/dhis2/composetable/model/TableModel.kt index c38a32ff77..6723520f0a 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/model/TableModel.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/model/TableModel.kt @@ -35,14 +35,17 @@ data class TableModel( ): Pair? = when { !successValidation -> cellSelection + cellSelection.columnIndex < tableHeaderModel.tableMaxColumns() - 1 -> cellSelection.copy(columnIndex = cellSelection.columnIndex + 1) + cellSelection.rowIndex < tableRows.size - 1 -> cellSelection.copy( columnIndex = 0, rowIndex = cellSelection.rowIndex + 1, globalIndex = cellSelection.globalIndex + 1, ) + else -> null }?.let { nextCell -> val tableCell = tableRows[nextCell.rowIndex].values[nextCell.columnIndex] @@ -57,10 +60,14 @@ data class TableModel( tableRows.size == 1 && tableRows.size == cell.rowIndex -> { tableRows[0].values[cell.columnIndex]?.takeIf { it.error != null } } + tableRows.size == cell.rowIndex -> { tableRows[cell.rowIndex - 1].values[cell.columnIndex]?.takeIf { it.error != null } } - else -> tableRows[cell.rowIndex].values[cell.columnIndex]?.takeIf { it.error != null } + + else -> tableRows.getOrNull(cell.rowIndex) + ?.values?.get(cell.columnIndex) + ?.takeIf { it.error != null } } } diff --git a/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt b/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt index fc3d10d08f..c60837de8e 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/model/TextInputModel.kt @@ -12,6 +12,7 @@ data class TextInputModel( val selection: TextRange? = null, val error: String? = null, val warning: String? = null, + val regex: Regex? = null, private val clearable: Boolean = false, ) { fun showClearButton() = clearable && currentValue?.isNotEmpty() == true diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt index 315b7e7b50..2a42e02c37 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/DataSetTableScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomSheetScaffold import androidx.compose.material.BottomSheetValue -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.icons.Icons @@ -53,6 +52,8 @@ import org.dhis2.composetable.ui.extensions.expandIfCollapsed import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor import org.hisp.dhis.mobile.ui.designsystem.component.InfoBar import org.hisp.dhis.mobile.ui.designsystem.component.InfoBarData +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType @OptIn(ExperimentalMaterialApi::class) @Composable @@ -153,9 +154,8 @@ fun DataSetTableScreen( if (isKeyboardOpen == Keyboard.Closed) { if (tableConfiguration.textInputViewMode) { focusManager.clearFocus(true) - } else if (bottomSheetState.bottomSheetState.isExpanded) { + } else { collapseBottomSheet(true) - bottomSheetState.bottomSheetState.collapse() } } } @@ -279,7 +279,7 @@ fun DataSetTableScreen( .background(Color.White), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator() + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) } } CompositionLocalProvider( diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/TableTheme.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/TableTheme.kt index 6e3af046c2..d8a6b758e7 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/TableTheme.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/TableTheme.kt @@ -1,6 +1,6 @@ package org.dhis2.composetable.ui -import androidx.compose.material.MaterialTheme +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import org.dhis2.composetable.actions.DefaultValidator diff --git a/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt index de38acc3d6..e238fb70d0 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/TextInput.kt @@ -209,14 +209,7 @@ private fun TextInputContent( }, value = textFieldValueState, onValueChange = { - textFieldValueState = it - onTextChanged( - textInputModel.copy( - currentValue = it.text, - selection = it.selection, - error = null, - ), - ) + textFieldValueState = manageOnValueChanged(textFieldValueState, it, onTextChanged, textInputModel) }, textStyle = TextStyle.Default.copy( fontSize = 12.sp, @@ -271,6 +264,32 @@ private fun TextInputContent( } } +fun manageOnValueChanged(textFieldValueState: TextFieldValue, newValue: TextFieldValue, onTextChanged: (TextInputModel) -> Unit, textInputModel: TextInputModel): TextFieldValue { + return if (textInputModel.regex != null) { + if (textInputModel.regex.matches(newValue.text)) { + onTextChanged( + textInputModel.copy( + currentValue = newValue.text, + selection = newValue.selection, + error = null, + ), + ) + newValue + } else { + textFieldValueState + } + } else { + onTextChanged( + textInputModel.copy( + currentValue = newValue.text, + selection = newValue.selection, + error = null, + ), + ) + newValue + } +} + @Composable private fun dividerColor(hasError: Boolean, hasWarning: Boolean, hasFocus: Boolean) = when { hasError -> LocalTableColors.current.errorColor diff --git a/compose-table/src/main/res/values-es/strings.xml b/compose-table/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..f034fb3115 --- /dev/null +++ b/compose-table/src/main/res/values-es/strings.xml @@ -0,0 +1,4 @@ + + + Aceptar + \ No newline at end of file diff --git a/compose-table/src/main/res/values-fr/strings.xml b/compose-table/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..a9cbe54a1b --- /dev/null +++ b/compose-table/src/main/res/values-fr/strings.xml @@ -0,0 +1,4 @@ + + + Accepter + \ No newline at end of file diff --git a/compose-table/src/main/res/values-pt/strings.xml b/compose-table/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..ae8e0576bc --- /dev/null +++ b/compose-table/src/main/res/values-pt/strings.xml @@ -0,0 +1,4 @@ + + + Aceitar + \ No newline at end of file diff --git a/compose-table/src/main/res/values-vi/strings.xml b/compose-table/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..f61cf63fa5 --- /dev/null +++ b/compose-table/src/main/res/values-vi/strings.xml @@ -0,0 +1,4 @@ + + + Chấp nhận + \ No newline at end of file diff --git a/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineExtensions.kt b/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineExtensions.kt index 7f64d9b8f5..525d1f5b74 100644 --- a/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineExtensions.kt +++ b/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RuleEngineExtensions.kt @@ -453,8 +453,6 @@ fun List.toRuleDataValue( value = "" } RuleDataValue( - eventDate = event.eventDate()!!.toRuleEngineInstant(), - programStage = event.programStage()!!, dataElement = it.dataElement()!!, value = value!!, ) diff --git a/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RulesRepository.kt b/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RulesRepository.kt index 5b9653123c..7a90af3811 100644 --- a/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RulesRepository.kt +++ b/dhis2-mobile-program-rules/src/main/java/org/dhis2/mobileProgramRules/RulesRepository.kt @@ -2,6 +2,7 @@ package org.dhis2.mobileProgramRules import android.os.Build import android.text.TextUtils.isEmpty +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -115,6 +116,9 @@ class RulesRepository(private val d2: D2) { .uid( event.organisationUnit(), ).blockingGet()?.code(), + createdDate = event.created() + ?.let { Instant.fromEpochMilliseconds(it.time) } + ?: Clock.System.now(), dataValues = event.trackedEntityDataValues()?.toRuleDataValue( event, d2.dataElementModule().dataElements(), @@ -134,6 +138,7 @@ class RulesRepository(private val d2: D2) { .byUid().notIn(eventToEvaluate.uid()) .byStatus().notIn(EventStatus.SCHEDULE, EventStatus.SKIPPED, EventStatus.OVERDUE) .byEventDate().beforeOrEqual(Date()) + .byDeleted().isFalse .withTrackedEntityDataValues() .orderByEventDate(RepositoryScope.OrderByDirection.DESC) .blockingGet() @@ -144,6 +149,7 @@ class RulesRepository(private val d2: D2) { .byOrganisationUnitUid().eq(eventToEvaluate.organisationUnit()) .byStatus().notIn(EventStatus.SCHEDULE, EventStatus.SKIPPED, EventStatus.OVERDUE) .byEventDate().beforeOrEqual(Date()) + .byDeleted().isFalse .withTrackedEntityDataValues() .orderByEventDate(RepositoryScope.OrderByDirection.DESC) .blockingGet().let { list -> @@ -180,6 +186,7 @@ class RulesRepository(private val d2: D2) { return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) .byStatus().notIn(EventStatus.SCHEDULE, EventStatus.SKIPPED, EventStatus.OVERDUE) .byEventDate().beforeOrEqual(Date()) + .byDeleted().isFalse .withTrackedEntityDataValues() .blockingGet() .map { event -> @@ -202,6 +209,9 @@ class RulesRepository(private val d2: D2) { organisationUnitCode = d2.organisationUnitModule() .organisationUnits().uid(event.organisationUnit()) .blockingGet()?.code(), + createdDate = event.created() + ?.let { Instant.fromEpochMilliseconds(it.time) } + ?: Clock.System.now(), dataValues = event.trackedEntityDataValues()?.toRuleDataValue( event, @@ -319,6 +329,9 @@ class RulesRepository(private val d2: D2) { completedDate = event.completedDate()?.toRuleEngineLocalDate(), organisationUnit = event.organisationUnit()!!, organisationUnitCode = d2.organisationUnit(event.organisationUnit()!!)?.code(), + createdDate = event.created() + ?.let { Instant.fromEpochMilliseconds(it.time) } + ?: Clock.System.now(), dataValues = emptyList(), ) } diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/di/Injector.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/di/Injector.kt index 84315ed7c7..45d6d9a9be 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/di/Injector.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/di/Injector.kt @@ -17,7 +17,6 @@ import org.hisp.dhis.android.core.D2Manager import org.hisp.dhis.android.core.common.FeatureType object Injector { - @Suppress("UNCHECKED_CAST") fun provideMapSelectorViewModelFactory( context: Context, locationType: FeatureType, diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapScreen.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapScreen.kt index f4401b5b89..ca390f532e 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapScreen.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapScreen.kt @@ -5,16 +5,23 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Icon +import androidx.compose.material3.Icon 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.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.dhis2.maps.R import org.dhis2.maps.location.LocationState @@ -34,7 +41,15 @@ fun MapScreen( onItem: @Composable LazyItemScope.(item: MapItemModel) -> Unit, ) { - Box(modifier = Modifier.fillMaxSize()) { + var pagerMaxHeight by remember { mutableStateOf(Dp.Unspecified) } + + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { + pagerMaxHeight = (it.size.height * 0.7).dp + }, + ) { map() Column( modifier = Modifier @@ -46,7 +61,8 @@ fun MapScreen( MapItemHorizontalPager( modifier = Modifier .align(Alignment.BottomCenter) - .testTag("MAP_CAROUSEL"), + .testTag("MAP_CAROUSEL") + .heightIn(max = pagerMaxHeight), state = listState, items = items, onItem = onItem, diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorScreen.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorScreen.kt index c7e1907413..199d6c3b13 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorScreen.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -77,10 +78,13 @@ import org.hisp.dhis.mobile.ui.designsystem.component.LocationBar import org.hisp.dhis.mobile.ui.designsystem.component.LocationItem import org.hisp.dhis.mobile.ui.designsystem.component.LocationItemIcon import org.hisp.dhis.mobile.ui.designsystem.component.OnSearchAction +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType import org.hisp.dhis.mobile.ui.designsystem.component.SearchBarMode import org.hisp.dhis.mobile.ui.designsystem.component.TopBar import org.hisp.dhis.mobile.ui.designsystem.component.model.LocationItemModel import org.hisp.dhis.mobile.ui.designsystem.theme.Shape +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @@ -148,6 +152,7 @@ fun SinglePaneMapSelector( captureMode = screenState.captureMode, selectedLocation = screenState.selectedLocation, searchOnThisAreaVisible = screenState.searchOnAreaVisible, + searching = screenState.searching, locationState = screenState.locationState, isManualCaptureEnabled = screenState.isManualCaptureEnabled, isPolygonMode = screenState.displayPolygonInfo, @@ -239,6 +244,7 @@ private fun TwoPaneMapSelector( captureMode = screenState.captureMode, selectedLocation = screenState.selectedLocation, searchOnThisAreaVisible = screenState.searchOnAreaVisible, + searching = screenState.searching, locationState = screenState.locationState, isManualCaptureEnabled = screenState.isManualCaptureEnabled, isPolygonMode = screenState.displayPolygonInfo, @@ -442,6 +448,7 @@ private fun Map( captureMode: MapSelectorViewModel.CaptureMode, selectedLocation: SelectedLocation, searchOnThisAreaVisible: Boolean, + searching: Boolean, locationState: LocationState, isManualCaptureEnabled: Boolean, isPolygonMode: Boolean, @@ -487,7 +494,7 @@ private fun Map( modifier = Modifier .align(Alignment.TopCenter) .offset(y = 60.dp), - visible = searchOnThisAreaVisible, + visible = searchOnThisAreaVisible or searching, enter = scaleIn( animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, @@ -502,6 +509,7 @@ private fun Map( ), ) { SearchInAreaButton( + searching = searching, onClick = onSearchOnAreaClick, ) } @@ -548,12 +556,24 @@ private fun SwipeToChangeLocationInfo( @Composable private fun SearchInAreaButton( + searching: Boolean, onClick: () -> Unit = {}, ) { Button( style = ButtonStyle.TONAL, - text = stringResource(R.string.search_on_this_area), - onClick = onClick, + text = if (searching) "" else stringResource(R.string.search_on_this_area), + icon = if (searching) { + { ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) } + } else { + null + }, + paddingValues = PaddingValues( + Spacing.Spacing24, + Spacing.Spacing10, + if (searching) Spacing.Spacing24 - Spacing.Spacing8 else Spacing.Spacing24, + Spacing.Spacing10, + ), + onClick = { if (searching.not()) onClick() }, ) } @@ -669,6 +689,7 @@ private class ScreenStateParamProvider : PreviewParameterProviderMostrar eventos Mostrar coordenadas de la entidad Mostrar mapa de calor - \ No newline at end of file + No se encontró navegador web instalado en el dispositivo, no se puede abrir la página web + Rango de precisión + Buena + Bajo + Medio + Muy buena + Espere para mejorar la precisión... + Su posición actual no está disponible + Ésta es la mejor precisión que pudimos obtener. + Se requiere una precisión mínima de %dm para guardar + ¿Quieres guardarla? + Buscar en este área + Desliza para cambiar de ubicación + Seleccione ubicación + pulsando sobre ella + Ubicación seleccionada + Lat: %1$s, Lon: %2$s + Suelta para seleccionar ubicación + \ No newline at end of file diff --git a/dhis2_android_maps/src/main/res/values-pt/strings.xml b/dhis2_android_maps/src/main/res/values-pt/strings.xml index 59a77532f3..2be6fdd418 100644 --- a/dhis2_android_maps/src/main/res/values-pt/strings.xml +++ b/dhis2_android_maps/src/main/res/values-pt/strings.xml @@ -1,10 +1,34 @@ - Selecionar local + Selecionar localização Camadas do mapa + Abrir mapa de ruas leve + OSM Detalhado  + Estrada Bing + Bing escuro + Bing Aerial + Etiquetas aéreas do Bing Aplicar Mostrar vista de satélite Mostrar eventos Mostrar coordenadas tei Mostrar camada do mapa de calor - \ No newline at end of file + Sem navegador web instalado no dispositivo, não é possível abrir a página web. + Faixa de precisão + Bom + Baixa + Média + Muito bom + Aguarde uma melhor precisão… + A sua posição atual não está disponível. + Esta é a melhor precisão que conseguimos obter. + É necessária uma precisão mínima de %dpara guardar + Queres salvá-lo? + Pesquisar nesta área + Deslize para alterar a localização + Selecionar localização + clicando no mesmo + Localização selecionada + Lat: %1$s, Lon: %2$s + Soltar para selecionar a localização + \ No newline at end of file diff --git a/dhis2_android_maps/src/main/res/values-vi/strings.xml b/dhis2_android_maps/src/main/res/values-vi/strings.xml index 1c7e5b60b9..e315ec8b87 100644 --- a/dhis2_android_maps/src/main/res/values-vi/strings.xml +++ b/dhis2_android_maps/src/main/res/values-vi/strings.xml @@ -2,9 +2,33 @@ Chọn địa điểm Các lớp bản đồ + OSM Light + OSM Detailed + Bing Road + Bing Dark + Bing Aerial + Big Aerial Labels Áp dụng Xem Vệ Tinh Hiển thị sự kiện Hiển thị tọa độ đối tượng theo dõi Hiển thị lớp bản đồ nhiệt độ - \ No newline at end of file + Trình duyệt web chưa được cài đặt trên thiết bị, không thể mở trang web. + Khoản chính xác + Tốt + Thấp + Trung bình + Rất tốt + Chờ đợi sự chính xác hơn... + Vị trí hiện tại của bạn không hiển thị + Đây là vị trí chính xác nhất chúng ta có thể lấy + Yêu cầu độ chính xác tối thiểu %dm để lưu + Bạn có muốn lưu nó không? + Tìm trong vùng này + Vuốt để thay đổi vị trí + Chọn vị trí + bằng cách bấm vào nó + Chọn vị trí + Lat: %1$s, Lon: %2$s + Thả để chọn vị trí + \ No newline at end of file diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ChartsRepositoryImpl.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ChartsRepositoryImpl.kt index c948c543b8..5e07432766 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ChartsRepositoryImpl.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ChartsRepositoryImpl.kt @@ -311,9 +311,19 @@ class ChartsRepositoryImpl( if (filters.isNotEmpty()) { filters.forEach { (columnIndex, value) -> lineListHeaderCache[trackerVisualizationUid]?.get(columnIndex)?.let { - filteredRepository = filteredRepository.withColumn( - column = it.withFilters(value), - ) + if (it is TrackerLineListItem.Category) { + val filterCategories = d2.categoryModule().categoryOptions() + .byDisplayName().like(value) + .blockingGetUids() + + filteredRepository = filteredRepository.withColumn( + column = it.withFilters(value, filterCategories), + ) + } else { + filteredRepository = filteredRepository.withColumn( + column = it.withFilters(value), + ) + } } } } diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/bindings/LineListingExtensions.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/bindings/LineListingExtensions.kt index 62ffd52bed..a02757545e 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/bindings/LineListingExtensions.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/bindings/LineListingExtensions.kt @@ -9,25 +9,28 @@ import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.hisp.dhis.android.core.common.RelativeOrganisationUnit import org.hisp.dhis.android.core.common.RelativePeriod -fun TrackerLineListItem.withFilters(value: String): TrackerLineListItem { +fun TrackerLineListItem.withFilters( + value: String, + categories: List = emptyList(), +): TrackerLineListItem { return when (this) { TrackerLineListItem.CreatedBy -> this TrackerLineListItem.LastUpdatedBy -> this is TrackerLineListItem.EnrollmentDate -> this.copy( filters = this.filters + listOf( - DateFilter.Range(value, value), + DateFilter.Like(value), ), ) is TrackerLineListItem.EventDate -> this.copy( filters = this.filters + listOf( - DateFilter.Range(value, value), + DateFilter.Like(value), ), ) is TrackerLineListItem.ScheduledDate -> this.copy( filters = this.filters + listOf( - DateFilter.Range(value, value), + DateFilter.Like(value), ), ) @@ -39,19 +42,19 @@ fun TrackerLineListItem.withFilters(value: String): TrackerLineListItem { is TrackerLineListItem.IncidentDate -> this.copy( filters = this.filters + listOf( - DateFilter.Range(value, value), + DateFilter.Like(value), ), ) is TrackerLineListItem.LastUpdated -> this.copy( filters = this.filters + listOf( - DateFilter.Range(value, value), + DateFilter.Like(value), ), ) is TrackerLineListItem.OrganisationUnitItem -> this.copy( filters = this.filters + listOf( - OrganisationUnitFilter.Absolute(value), + OrganisationUnitFilter.Like(value), ), ) @@ -78,10 +81,9 @@ fun TrackerLineListItem.withFilters(value: String): TrackerLineListItem { EnumFilter.Like(value), ), ) - is TrackerLineListItem.Category -> this.copy( filters = this.filters + listOf( - DataFilter.Like(value), + DataFilter.In(categories), ), ) } diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/data/GraphFilters.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/data/GraphFilters.kt index 3d9ccb171f..a74a6659ac 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/data/GraphFilters.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/data/GraphFilters.kt @@ -41,9 +41,7 @@ sealed class GraphFilters { lineListFilters.isNotEmpty() || orgUnitsSelected.isNotEmpty() || periodToDisplaySelected.isNotEmpty() override fun count(): Int { - var count = 0 - if (hasFilters()) count++ - return count + return columnsWithFilters().size } override fun canDisplayChart(dataIsNotEmpty: Boolean): Boolean { diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/idling/AnalyticsCountingIdlingResource.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/idling/AnalyticsCountingIdlingResource.kt new file mode 100644 index 0000000000..b8085bd75e --- /dev/null +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/idling/AnalyticsCountingIdlingResource.kt @@ -0,0 +1,19 @@ +package dhis2.org.analytics.charts.idling + +import androidx.test.espresso.idling.CountingIdlingResource + +object AnalyticsCountingIdlingResource { + private const val RESOURCE = "ANALYTICS" + + @JvmField + val countingIdlingResource = CountingIdlingResource(RESOURCE) + fun increment() { + countingIdlingResource.increment() + } + + fun decrement() { + if (!countingIdlingResource.isIdleNow) { + countingIdlingResource.decrement() + } + } +} diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToTable.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToTable.kt index efdf436adc..19c7950da7 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToTable.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/mappers/GraphToTable.kt @@ -4,8 +4,8 @@ import android.content.res.Configuration import android.view.View import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -16,9 +16,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp -import com.google.android.material.composethemeadapter.MdcTheme +import androidx.compose.ui.unit.sp import dhis2.org.R import dhis2.org.analytics.charts.data.ChartType import dhis2.org.analytics.charts.data.Graph @@ -41,8 +46,9 @@ import org.dhis2.composetable.ui.TableDimensions import org.dhis2.composetable.ui.TableSelection import org.dhis2.composetable.ui.TableTheme import org.dhis2.composetable.ui.compositions.LocalInteraction -import org.dhis2.ui.theme.descriptionTextStyle import org.hisp.dhis.android.core.arch.helpers.DateUtils +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import kotlin.math.roundToInt private const val LINE_LISTING_MAX_ROWS = 500 @@ -70,7 +76,7 @@ class GraphToTable { dimensionsForLinelisting(localDensity, conf) } - return MdcTheme { + return DHIS2Theme { var dimensions by remember { mutableStateOf( tableDimensions, @@ -117,8 +123,8 @@ class GraphToTable { TableTheme( tableColors = TableColors( - primary = MaterialTheme.colors.primary, - primaryLight = MaterialTheme.colors.primary.copy(alpha = 0.2f), + primary = MaterialTheme.colorScheme.primary, + primaryLight = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), disabledCellText = TableTheme.colors.cellText, disabledCellBackground = TableTheme.colors.tableBackground, ), @@ -169,7 +175,15 @@ class GraphToTable { R.string.line_listing_max_results, LINE_LISTING_MAX_ROWS, ), - style = descriptionTextStyle, + style = TextStyle( + color = TextColor.OnSurfaceLight, + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + fontFamily = FontFamily(Font(org.dhis2.ui.R.font.roboto_regular)), + lineHeight = 16.sp, + letterSpacing = (0.4).sp, + textAlign = TextAlign.End, + ), ) } } diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/ChartViewHolder.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/ChartViewHolder.kt index 19a0a76105..afdc76eb62 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/ChartViewHolder.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/ChartViewHolder.kt @@ -7,11 +7,11 @@ import androidx.databinding.Observable import androidx.recyclerview.widget.RecyclerView import androidx.transition.Slide import androidx.transition.TransitionManager -import com.google.android.material.composethemeadapter.MdcTheme import dhis2.org.analytics.charts.data.ChartType import dhis2.org.analytics.charts.data.toChartBuilder import dhis2.org.databinding.ItemChartBinding import org.hisp.dhis.android.core.common.RelativePeriod +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme class ChartViewHolder( val binding: ItemChartBinding, @@ -74,7 +74,7 @@ class ChartViewHolder( private fun loadComposeChart(chart: ChartModel, visible: Boolean = true) { binding.composeChart.setContent { - MdcTheme { + DHIS2Theme { if (visible) { binding.chartContainer.removeAllViews() chart.graph.toChartBuilder() diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsFragment.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsFragment.kt index 6b785bbbad..71207f1dca 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsFragment.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsFragment.kt @@ -13,6 +13,7 @@ import dhis2.org.R import dhis2.org.analytics.charts.data.AnalyticGroup import dhis2.org.analytics.charts.di.AnalyticsComponentProvider import dhis2.org.analytics.charts.extensions.isNotCurrent +import dhis2.org.analytics.charts.idling.AnalyticsCountingIdlingResource import dhis2.org.analytics.charts.ui.di.AnalyticsFragmentModule import dhis2.org.analytics.charts.ui.dialog.SearchColumnDialog import dhis2.org.databinding.AnalyticsGroupBinding @@ -250,6 +251,7 @@ class GroupAnalyticsFragment : Fragment() { } } } + AnalyticsCountingIdlingResource.decrement() } } diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModel.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModel.kt index 721130a132..5855281f93 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModel.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dhis2.org.analytics.charts.Charts import dhis2.org.analytics.charts.data.AnalyticGroup +import dhis2.org.analytics.charts.idling.AnalyticsCountingIdlingResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch @@ -119,6 +120,7 @@ class GroupAnalyticsViewModel( } fun fetchAnalytics(groupUid: String?) { + AnalyticsCountingIdlingResource.increment() currentGroup = groupUid viewModelScope.launch { val result = async(context = Dispatchers.IO) { diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModelFactory.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModelFactory.kt index a0d68ee7f3..89f61b70fa 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModelFactory.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/GroupAnalyticsViewModelFactory.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModelProvider import dhis2.org.analytics.charts.Charts import org.dhis2.commons.matomo.MatomoAnalyticsController -@Suppress("UNCHECKED_CAST") class GroupAnalyticsViewModelFactory( private val mode: AnalyticMode, private val uid: String?, diff --git a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/IndicatorViewHolder.kt b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/IndicatorViewHolder.kt index 272e71ee5b..37e0c997ba 100644 --- a/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/IndicatorViewHolder.kt +++ b/dhis_android_analytics/src/main/java/dhis2/org/analytics/charts/ui/IndicatorViewHolder.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,7 +40,7 @@ class IndicatorViewHolder( if (programIndicatorModel.programIndicator?.description() != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = { showDescription(programIndicatorModel.programIndicator) }, ) } else { @@ -60,7 +60,7 @@ class IndicatorViewHolder( if (programIndicatorModel.programIndicator?.description() != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), + indication = ripple(), onClick = { showDescription(programIndicatorModel.programIndicator) }, ) } else { diff --git a/dhis_android_analytics/src/main/res/values-pt/strings.xml b/dhis_android_analytics/src/main/res/values-pt/strings.xml index 89953e5086..c67cf67ebd 100644 --- a/dhis_android_analytics/src/main/res/values-pt/strings.xml +++ b/dhis_android_analytics/src/main/res/values-pt/strings.xml @@ -63,4 +63,8 @@ A visualização não esta bem configurada, contacte o seu admin Limites personalizados não são suportados para o indicador de programa %s Não consegue aceder a base de dados. Favor contactar o seu administrador. - \ No newline at end of file + Não tem acesso ao programa%s + Não tem acesso ao atributo %s + Tipo de agregação não suportado %s + Tabela limitada a %1$s entradas. Filtrar para refinar os resultados. + \ No newline at end of file diff --git a/dhis_android_analytics/src/main/res/values-vi/strings.xml b/dhis_android_analytics/src/main/res/values-vi/strings.xml index 0cff6c9c5f..c752921c92 100644 --- a/dhis_android_analytics/src/main/res/values-vi/strings.xml +++ b/dhis_android_analytics/src/main/res/values-vi/strings.xml @@ -65,4 +65,6 @@ Không thể truy cập cơ sở dữ liệu. Xin vui lòng liên hệ quản lý của bạn. Bạn không có quyền truy cập đến chương trình %s Bạn không có quyền truy cập đến thuộc tính %s + Kiểu tổng hợp chưa được hỗ trợ%s + Bảng báo cáo chỉ giới hạn đến %1$smục. Hãy lọc để tinh chỉnh kết quả \ No newline at end of file diff --git a/dhis_android_analytics/src/test/java/dhis2/org/analytics/charts/bindings/LineListingExtensionsTest.kt b/dhis_android_analytics/src/test/java/dhis2/org/analytics/charts/bindings/LineListingExtensionsTest.kt index bf5ef59419..ca1a2f4878 100644 --- a/dhis_android_analytics/src/test/java/dhis2/org/analytics/charts/bindings/LineListingExtensionsTest.kt +++ b/dhis_android_analytics/src/test/java/dhis2/org/analytics/charts/bindings/LineListingExtensionsTest.kt @@ -1,5 +1,6 @@ package dhis2.org.analytics.charts.bindings +import org.hisp.dhis.android.core.analytics.trackerlinelist.DataFilter import org.hisp.dhis.android.core.analytics.trackerlinelist.TrackerLineListItem import org.junit.Test @@ -14,8 +15,9 @@ class LineListingExtensionsTest { assert(item.filters.isEmpty()) - val result = item.withFilters("categoryDisplayName") + val result = item.withFilters("categoryDisplayName", listOf("categoryUid")) assert((result as TrackerLineListItem.Category).filters.size == 1) + assert((result.filters.first() as DataFilter.In).values.isNotEmpty()) } } diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt index 5572dfe725..0c4ae89019 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingData import io.reactivex.Flowable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import org.dhis2.commons.periods.model.Period import org.dhis2.form.model.EventMode import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetConfiguration @@ -41,6 +42,7 @@ interface DataEntryRepository { fun disableCollapsableSections(): Boolean? fun getSpecificDataEntryItems(uid: String): List + fun fetchPeriods(): Flow> fun options( optionSetUid: String, diff --git a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt index 1cad5159f9..723ea6c117 100644 --- a/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EnrollmentRepository.kt @@ -1,9 +1,13 @@ package org.dhis2.form.data +import androidx.paging.PagingData import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import org.dhis2.commons.date.DateUtils import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.model.Period import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.form.data.metadata.EnrollmentConfiguration import org.dhis2.form.model.EnrollmentMode @@ -70,6 +74,8 @@ class EnrollmentRepository( } } + override fun fetchPeriods(): Flow> = emptyFlow() + override fun sectionUids(): Flowable> { val sectionUids = mutableListOf(ENROLLMENT_DATA_SECTION_UID) if (programSections.isEmpty()) { diff --git a/form/src/main/java/org/dhis2/form/data/EventRepository.kt b/form/src/main/java/org/dhis2/form/data/EventRepository.kt index c684137015..c51f085c79 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -1,8 +1,11 @@ package org.dhis2.form.data import android.text.TextUtils +import androidx.paging.PagingData import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import org.dhis2.bindings.blockingGetValueCheck import org.dhis2.bindings.userFriendlyValue import org.dhis2.commons.bindings.program @@ -10,6 +13,9 @@ import org.dhis2.commons.date.DateUtils import org.dhis2.commons.extensions.inDateRange import org.dhis2.commons.extensions.inOrgUnit import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.data.EventPeriodRepository +import org.dhis2.commons.periods.domain.GetEventPeriods +import org.dhis2.commons.periods.model.Period import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager @@ -38,12 +44,10 @@ import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.imports.ImportStatus import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.program.ProgramStageDataElement import org.hisp.dhis.android.core.program.ProgramStageSection import org.hisp.dhis.android.core.program.SectionRenderingType import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor -import java.util.Date class EventRepository( private val fieldFactory: FieldViewModelFactory, @@ -56,6 +60,10 @@ class EventRepository( private val eventMode: EventMode, ) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory, metadataIconProvider) { + private val getEventPeriods = GetEventPeriods( + EventPeriodRepository(d2), + ) + private var event = d2.eventModule().events().uid(eventUid).blockingGet() private val programStage by lazy { @@ -184,7 +192,7 @@ class EventRepository( return true } - override fun eventMode(): EventMode? { + override fun eventMode(): EventMode { return eventMode } @@ -427,8 +435,8 @@ class EventRepository( if (periodType != PeriodType.Daily) { PeriodSelector( type = periodType, - minDate = getPeriodMinDate(periodType), - maxDate = dateUtils.getStartOfDay(Date()), + minDate = null, + maxDate = null, ) } else { null @@ -436,98 +444,6 @@ class EventRepository( } } - private fun getPeriodMinDate(periodType: PeriodType): Date? { - d2.programModule().programs() - .withTrackedEntityType() - .byUid().eq(programUid) - .one().blockingGet()?.let { program -> - val firstAvailablePeriodDate = - getFirstAvailablePeriod(event?.enrollment(), programStage) - var minDate = dateUtils.expDate( - firstAvailablePeriodDate, - program.expiryDays() ?: 0, - periodType, - ) - val lastPeriodDate = dateUtils.getNextPeriod( - periodType, - minDate, - -1, - true, - ) - - if (lastPeriodDate.after( - dateUtils.getNextPeriod( - program.expiryPeriodType(), - minDate, - 0, - ), - ) - ) { - minDate = dateUtils.getNextPeriod(periodType, lastPeriodDate, 0) - } - return minDate - } - return null - } - - private fun getFirstAvailablePeriod(enrollmentUid: String?, programStage: ProgramStage?): Date { - val stageLastDate = getStageLastDate() - val minEventDate = stageLastDate ?: when (programStage?.generatedByEnrollmentDate()) { - true -> getEnrollmentDate(enrollmentUid) - else -> getEnrollmentIncidentDate(enrollmentUid) - ?: getEnrollmentDate(enrollmentUid) - } - val calendar = DateUtils.getInstance().getCalendarByDate(minEventDate) - - return dateUtils.getNextPeriod( - programStage?.periodType(), - calendar.time ?: event?.eventDate(), - if (stageLastDate == null) 0 else 1, - ) - } - - private fun getStageLastDate(): Date? { - val enrollmentUid = event?.enrollment() - val programStageUid = programStage?.uid() - val activeEvents = - d2.eventModule().events().byEnrollmentUid() - .eq(enrollmentUid).byProgramStageUid() - .eq(programStageUid) - .byDeleted().isFalse - .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet() - .filter { it.uid() != eventUid } - val scheduleEvents = - d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).byProgramStageUid() - .eq(programStageUid) - .byDeleted().isFalse - .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet() - .filter { it.uid() != eventUid } - - var activeDate: Date? = null - var scheduleDate: Date? = null - if (activeEvents.isNotEmpty()) { - activeDate = activeEvents[0].eventDate() - } - if (scheduleEvents.isNotEmpty()) scheduleDate = scheduleEvents[0].dueDate() - - return when { - scheduleDate == null -> activeDate - activeDate == null -> scheduleDate - activeDate.before(scheduleDate) -> scheduleDate - else -> activeDate - } - } - - private fun getEnrollmentDate(uid: String?): Date? { - val enrollment = d2.enrollmentModule().enrollments().byUid().eq(uid).blockingGet().first() - return enrollment.enrollmentDate() - } - - private fun getEnrollmentIncidentDate(uid: String?): Date? { - val enrollment = d2.enrollmentModule().enrollments().uid(uid).blockingGet() - return enrollment?.incidentDate() - } - private fun createEventDetailsSection(): FieldUiModel { return fieldFactory.createSection( sectionUid = EVENT_DETAILS_SECTION_UID, @@ -558,6 +474,24 @@ class EventRepository( } } + override fun fetchPeriods(): Flow> { + val periodType = programStage?.periodType() ?: PeriodType.Daily + val stage = programStage ?: return flowOf() + val eventEnrollmentUid = event?.enrollment() ?: return flowOf() + return getEventPeriods( + eventUid = eventUid, + periodType = periodType, + selectedDate = if (eventMode == EventMode.SCHEDULE) { + event?.dueDate() + } else { + event?.eventDate() + }, + programStage = stage, + isScheduling = eventMode == EventMode.SCHEDULE, + eventEnrollmentUid = eventEnrollmentUid, + ) + } + private fun getFieldsForSingleSection(): Single> { return Single.fromCallable { val stageDataElements = 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 d3bef68f3d..c51cb1a8d7 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepository.kt @@ -1,5 +1,8 @@ package org.dhis2.form.data +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.model.Period import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.RowAction import org.dhis2.form.model.StoreResult @@ -32,5 +35,6 @@ interface FormRepository { fun getListFromPreferences(uid: String): MutableList fun saveListToPreferences(uid: String, list: List) fun activateEvent() + fun fetchPeriods(): Flow> fun fetchOptions(id: String, optionSetUid: String) } 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 0895fd5083..33f1d30f4c 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt @@ -1,7 +1,10 @@ package org.dhis2.form.data +import androidx.paging.PagingData import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.model.Period import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.form.data.EnrollmentRepository.Companion.ENROLLMENT_DATE_UID @@ -94,6 +97,10 @@ class FormRepositoryImpl( formValueStore.activateEvent() } + override fun fetchPeriods(): Flow> { + return dataEntryRepository.fetchPeriods() + } + private fun List.setLastItem(): List { if (isEmpty()) { return this @@ -234,7 +241,7 @@ class FormRepositoryImpl( return when { (itemsWithErrors.isEmpty() && itemsWithWarning.isEmpty() && mandatoryItemsWithoutValue.isEmpty()) -> { - getSuccessfulResult(eventStatus) + getSuccessfulResult() } (itemsWithErrors.isNotEmpty()) -> { @@ -285,7 +292,7 @@ class FormRepositoryImpl( EventStatus.COMPLETED -> { FieldsWithWarningResult( fieldUidWarningList = itemsWithWarning, - canComplete = false, + canComplete = ruleEffectsResult?.canComplete ?: false, onCompleteMessage = ruleEffectsResult?.messageOnComplete, eventResultDetails = EventResultDetails( formValueStore.eventState(), @@ -427,44 +434,16 @@ class FormRepositoryImpl( } } - private fun getSuccessfulResult(eventStatus: EventStatus?): SuccessfulResult { - return when (eventStatus) { - EventStatus.ACTIVE -> { - SuccessfulResult( - canComplete = ruleEffectsResult?.canComplete ?: true, - onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails( - formValueStore.eventState(), - dataEntryRepository.eventMode(), - dataEntryRepository.validationStrategy(), - ), - ) - } - - EventStatus.COMPLETED -> { - SuccessfulResult( - canComplete = false, - onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails( - formValueStore.eventState(), - dataEntryRepository.eventMode(), - dataEntryRepository.validationStrategy(), - ), - ) - } - - else -> { - SuccessfulResult( - canComplete = ruleEffectsResult?.canComplete ?: false, - onCompleteMessage = ruleEffectsResult?.messageOnComplete, - eventResultDetails = EventResultDetails( - formValueStore.eventState(), - dataEntryRepository.eventMode(), - validationStrategy = dataEntryRepository.validationStrategy(), - ), - ) - } - } + private fun getSuccessfulResult(): SuccessfulResult { + return SuccessfulResult( + canComplete = ruleEffectsResult?.canComplete ?: true, + onCompleteMessage = ruleEffectsResult?.messageOnComplete, + eventResultDetails = EventResultDetails( + formValueStore.eventState(), + dataEntryRepository.eventMode(), + dataEntryRepository.validationStrategy(), + ), + ) } override fun completedFieldsPercentage(value: List): Float { diff --git a/form/src/main/java/org/dhis2/form/data/RulesUtilsProviderImpl.kt b/form/src/main/java/org/dhis2/form/data/RulesUtilsProviderImpl.kt index 238325287c..7711fe459a 100644 --- a/form/src/main/java/org/dhis2/form/data/RulesUtilsProviderImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/RulesUtilsProviderImpl.kt @@ -385,6 +385,9 @@ class RulesUtilsProviderImpl( val message = warningOnCompletion.content() + " " + data if (model != null) { fieldViewModels[fieldUid] = model.setWarning(message) + fieldsWithWarnings.add( + FieldWithError(fieldUid, message), + ) } messageOnComplete = message diff --git a/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt b/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt index be2a049e7f..c01908c685 100644 --- a/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt +++ b/form/src/main/java/org/dhis2/form/model/FieldUiModelImpl.kt @@ -61,7 +61,9 @@ data class FieldUiModelImpl( override fun invokeUiEvent(uiEventType: UiEventType) { callback?.intent(FormIntent.OnRequestCoordinates(uid)) - + if (!focused) { + onItemClick() + } uiEventFactory?.generateEvent(value, uiEventType, renderingType, this)?.let { callback?.recyclerViewUiEvents(it) } 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 83388f2477..f3f4b34681 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormView.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormView.kt @@ -25,6 +25,7 @@ import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels +import androidx.paging.compose.collectAsLazyPagingItems import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.journeyapps.barcodescanner.ScanOptions import org.dhis2.commons.ActivityResultObservable @@ -38,12 +39,12 @@ import org.dhis2.commons.data.FormFileProvider import org.dhis2.commons.date.DateUtils import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.CustomDialog -import org.dhis2.commons.dialogs.PeriodDialog import org.dhis2.commons.extensions.closeKeyboard import org.dhis2.commons.extensions.serializable import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.ui.PeriodSelectorContent import org.dhis2.form.R import org.dhis2.form.data.DataIntegrityCheckResult import org.dhis2.form.data.FieldsWithErrorResult @@ -74,15 +75,14 @@ import org.dhis2.maps.views.MapSelectorActivity.Companion.LOCATION_TYPE_EXTRA import org.dhis2.ui.ErrorFieldList import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel +import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue -import org.dhis2.ui.dialogs.bottomsheet.IssueType import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.common.ValueTypeRenderingType import org.hisp.dhis.android.core.event.EventStatus import timber.log.Timber import java.io.File -import java.util.Date class FormView : Fragment() { @@ -148,7 +148,7 @@ class FormView : Fragment() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -426,13 +426,12 @@ class FormView : Fragment() { }, onMainButtonClicked = { bottomSheetDialog -> manageMainButtonAction( - fieldsWithIssues, - result.eventResultDetails.eventStatus == EventStatus.COMPLETED, + model.mainButton, bottomSheetDialog, ) }, showDivider = fieldsWithIssues.isNotEmpty(), - content = { bottomSheetDialog -> + content = { bottomSheetDialog, _ -> DialogContent(fieldsWithIssues, bottomSheetDialog = bottomSheetDialog) }, ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) @@ -463,19 +462,17 @@ class FormView : Fragment() { } private fun manageMainButtonAction( - fieldsWithIssues: List, - isEventCompleted: Boolean, + mainButtonModel: DialogButtonStyle?, bottomSheetDialog: BottomSheetDialog, ) { - val errorsInField = - fieldsWithIssues.isNotEmpty() || fieldsWithIssues.any { it.issueType == IssueType.ERROR } - if (errorsInField) { - bottomSheetDialog.dismiss() - } else if (isEventCompleted) { - onFinishDataEntry?.invoke() - } else { - viewModel.completeEvent() - onFinishDataEntry?.invoke() + when (mainButtonModel) { + DialogButtonStyle.CompleteButton -> { + viewModel.completeEvent() + onFinishDataEntry?.invoke() + } + else -> { + bottomSheetDialog.dismiss() + } } } @@ -565,22 +562,34 @@ class FormView : Fragment() { } private fun showPeriodDialog(uiEvent: RecyclerViewUiEvents.SelectPeriod) { - PeriodDialog() - .setTitle(uiEvent.title) - .setPeriod(uiEvent.periodType) - .setMinDate(uiEvent.minDate) - .setMaxDate(uiEvent.maxDate) - .setPossitiveListener { selectedDate: Date -> - val dateString = DateUtils.oldUiDateFormat().format(selectedDate) - intentHandler( - FormIntent.OnSave( - uiEvent.uid, - dateString, - ValueType.DATE, - ), - ) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = uiEvent.title, + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { _ -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + val dateString = DateUtils.oldUiDateFormat().format(selectedDate) + intentHandler( + FormIntent.OnSave( + uiEvent.uid, + dateString, + ValueType.DATE, + ), + ) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } private fun openChooserIntent(uiEvent: RecyclerViewUiEvents.OpenChooserIntent) { 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 50b9aab0b2..a017ba343f 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -6,16 +6,19 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import kotlinx.coroutines.async import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.periods.model.Period import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.R import org.dhis2.form.data.DataIntegrityCheckResult @@ -343,11 +346,16 @@ class FormViewModel( private fun saveLastFocusedItem(rowAction: RowAction) = getLastFocusedTextItem()?.let { if (previousActionItem == null) previousActionItem = rowAction if (previousActionItem?.value != it.value && previousActionItem?.id == it.uid) { - val error = checkFieldError(it.valueType, it.value, it.fieldMask) - if (error != null) { - val action = rowActionFromIntent( - FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask), - ) + val action = rowActionFromIntent( + FormIntent.OnSave( + it.uid, + it.value, + it.valueType, + it.fieldMask, + it.allowFutureDates, + ), + ) + if (action.error != null) { repository.updateErrorList(action) StoreResult( rowAction.id, @@ -355,8 +363,6 @@ class FormViewModel( ) } else { checkAutoCompleteForLastFocusedItem(it) - val intent = FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask) - val action = rowActionFromIntent(intent) val result = repository.save(it.uid, it.value, action.extraData) repository.updateValueOnList(it.uid, it.value, it.valueType) repository.updateErrorList(action) @@ -809,13 +815,13 @@ class FormViewModel( fun discardChanges() { repository.backupOfChangedItems().forEach { - submitIntent(FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask)) + submitIntent(FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask, it.allowFutureDates)) } } fun saveDataEntry() { getLastFocusedTextItem()?.let { - submitIntent(FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask)) + submitIntent(FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask, it.allowFutureDates)) } submitIntent(FormIntent.OnFinish()) } @@ -862,6 +868,10 @@ class FormViewModel( } } + fun fetchPeriods(): Flow> { + return repository.fetchPeriods().flowOn(dispatcher.io()) + } + companion object { const val TAG = "FormViewModel" } diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModelFactory.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModelFactory.kt index af9e204783..0f6e584bfb 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModelFactory.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModelFactory.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.FormRepository -@Suppress("UNCHECKED_CAST") class FormViewModelFactory( private val repository: FormRepository, private val dispatcher: DispatcherProvider, diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialog.kt b/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialog.kt index 39411fe4b3..10b8fe0609 100644 --- a/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialog.kt +++ b/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialog.kt @@ -11,11 +11,11 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.form.di.Injector import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetDialogViewModel import org.dhis2.form.model.OptionSetDialogViewModelFactory +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme const val TAG = "OptionSetDialog" @@ -50,7 +50,7 @@ class OptionSetDialog( ViewCompositionStrategy.DisposeOnDetachedFromWindow, ) setContent { - MdcTheme { + DHIS2Theme { OptionSetDialogScreen( viewModel, onCancelClick = { dismiss() }, diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialogUi.kt b/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialogUi.kt index 8588dc0986..5867b917a0 100644 --- a/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialogUi.kt +++ b/form/src/main/java/org/dhis2/form/ui/dialog/OptionSetDialogUi.kt @@ -18,15 +18,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -45,6 +44,8 @@ import org.dhis2.form.model.OptionSetDialogViewModel import org.hisp.dhis.android.core.option.Option import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType @Composable fun OptionSetDialogScreen( @@ -90,7 +91,7 @@ fun OptionSetDialogScreen( ) { when (searchValue.isNotEmpty()) { true -> Text(stringResource(R.string.no_option_found)) - else -> CircularProgressIndicator() + else -> ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) } } } @@ -131,7 +132,7 @@ private fun SearchBar( modifier = Modifier .border( width = 2.dp, - color = MaterialTheme.colors.primary, + color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(16.dp), ) .padding(horizontal = 12.dp), @@ -140,7 +141,7 @@ private fun SearchBar( Icon( imageVector = Icons.Filled.Search, contentDescription = "", - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.size(8.dp)) Box(Modifier.weight(1f)) { @@ -148,7 +149,7 @@ private fun SearchBar( Text( text = stringResource(id = R.string.search), style = LocalTextStyle.current.copy( - color = MaterialTheme.colors.onSurface.copy(alpha = 0.3f), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), fontSize = 16.sp, ), ) @@ -162,7 +163,7 @@ private fun SearchBar( Icon( imageVector = Icons.Filled.Clear, contentDescription = "", - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colorScheme.primary, ) }, ) 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 b2b948894c..0cb041c3d5 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 @@ -11,6 +11,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.FileDownload @@ -41,6 +42,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell import org.hisp.dhis.mobile.ui.designsystem.component.ButtonCarousel import org.hisp.dhis.mobile.ui.designsystem.component.CarouselButtonData import org.hisp.dhis.mobile.ui.designsystem.component.QrCodeBlock +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import timber.log.Timber import java.io.File @@ -144,7 +147,15 @@ QRDetailBottomDialog( } }, buttonBlock = { - ButtonCarousel(buttonList) + ButtonCarousel( + carouselButtonList = buttonList, + modifier = Modifier.padding( + top = Spacing0, + bottom = Spacing24, + start = Spacing24, + end = Spacing24, + ), + ) }, onDismiss = { dismiss() diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/QRImageViewModelFactory.kt b/form/src/main/java/org/dhis2/form/ui/dialog/QRImageViewModelFactory.kt index a8c8b3e4fe..7c052c1e38 100644 --- a/form/src/main/java/org/dhis2/form/ui/dialog/QRImageViewModelFactory.kt +++ b/form/src/main/java/org/dhis2/form/ui/dialog/QRImageViewModelFactory.kt @@ -3,7 +3,6 @@ package org.dhis2.form.ui.dialog import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -@Suppress("UNCHECKED_CAST") class QRImageViewModelFactory : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return QRImageViewModel(QRImageControllerImpl()) as T diff --git a/form/src/main/java/org/dhis2/form/ui/provider/FormResultDialogProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/FormResultDialogProvider.kt index 0d3b58e96c..727eab537a 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/FormResultDialogProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/FormResultDialogProvider.kt @@ -33,11 +33,15 @@ class FormResultDialogProvider( eventState: EventStatus?, result: DataIntegrityCheckResult, ): Pair> { + val onCompleteMessages = getOnCompleteMessage( + canComplete, + onCompleteMessage, + ) val dialogType = getDialogType( errorFields, emptyMandatoryFields, warningFields, - !canComplete && onCompleteMessage != null, + onCompleteMessages, ) val showSkipButton = when { dialogType == DialogType.WARNING || dialogType == DialogType.SUCCESSFUL -> true @@ -74,12 +78,14 @@ class FormResultDialogProvider( result.fieldUidErrorList, result.mandatoryFields.keys.toList(), result.warningFields, + onCompleteMessages, ) Pair(model, fieldsWithIssues) } is FieldsWithWarningResult -> { val fieldsWithIssues = getFieldsWithIssues( warningFields = result.fieldUidWarningList, + onCompleteFields = onCompleteMessages, ) return Pair(model, fieldsWithIssues) } @@ -101,7 +107,7 @@ class FormResultDialogProvider( Pair(notSavedModel, emptyList()) } is SuccessfulResult -> { - Pair(model, emptyList()) + Pair(model, onCompleteMessages) } } } @@ -166,44 +172,54 @@ class FormResultDialogProvider( errorFields: List = emptyList(), mandatoryFields: List = emptyList(), warningFields: List = emptyList(), + onCompleteFields: List = emptyList(), ): List { - return errorFields.plus( - mandatoryFields.map { - FieldWithIssue( - "uid", - it, - IssueType.MANDATORY, - provider.provideMandatoryField(), - ) - }, - ).plus(warningFields) + return onCompleteFields + .plus(errorFields) + .plus( + mandatoryFields.map { + FieldWithIssue( + "uid", + it, + IssueType.MANDATORY, + provider.provideMandatoryField(), + ) + }, + ).plus(warningFields) + } + + private fun getOnCompleteMessage( + canComplete: Boolean, + onCompleteMessage: String?, + ): List { + val issueOnComplete = onCompleteMessage?.let { + FieldWithIssue( + fieldUid = "", + fieldName = it, + issueType = when (canComplete) { + false -> IssueType.ERROR_ON_COMPLETE + else -> IssueType.WARNING_ON_COMPLETE + }, + message = "", + ) + } + return issueOnComplete?.let { listOf(it) } ?: emptyList() } private fun getDialogType( errorFields: List, mandatoryFields: Map, warningFields: List, - errorOnComplete: Boolean, + onCompleteFields: List, ) = when { - errorOnComplete -> { + onCompleteFields.any { it.issueType == IssueType.ERROR_ON_COMPLETE } -> DialogType.COMPLETE_ERROR - } - - errorFields.isNotEmpty() -> { - DialogType.ERROR - } - - mandatoryFields.isNotEmpty() -> { - DialogType.MANDATORY - } - - warningFields.isNotEmpty() -> { + errorFields.isNotEmpty() -> DialogType.ERROR + mandatoryFields.isNotEmpty() -> DialogType.MANDATORY + warningFields.isNotEmpty() || + onCompleteFields.any { it.issueType == IssueType.WARNING_ON_COMPLETE } -> DialogType.WARNING - } - - else -> { - DialogType.SUCCESSFUL - } + else -> DialogType.SUCCESSFUL } enum class DialogType { ERROR, MANDATORY, WARNING, SUCCESSFUL, COMPLETE_ERROR } } 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 41d9f3e396..f6f03c16c3 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 @@ -15,10 +15,10 @@ 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 import java.io.File +import java.text.DecimalFormat @Composable internal fun ProvideInputFileResource( @@ -27,17 +27,15 @@ internal fun ProvideInputFileResource( resources: ResourceManager, uiEventHandler: (RecyclerViewUiEvents) -> Unit, ) { - var uploadState by remember(fieldUiModel) { mutableStateOf(getFileUploadState(fieldUiModel.displayName, fieldUiModel.isLoadingData)) } - - val fileInputData = - fieldUiModel.displayName?.let { - val file = File(it) - InputData.FileInputData( - fileName = file.name, - fileSize = file.length(), - filePath = file.path, - ) - } + var uploadState by remember(fieldUiModel) { + mutableStateOf( + getFileUploadState( + fieldUiModel.displayName, + fieldUiModel.isLoadingData, + ), + ) + } + val file = fieldUiModel.displayName?.let { File(it) } InputFileResource( modifier = modifier.fillMaxWidth(), @@ -46,8 +44,8 @@ internal fun ProvideInputFileResource( supportingText = fieldUiModel.supportingText(), buttonText = resources.getString(R.string.add_file), uploadFileState = uploadState, - fileName = fileInputData?.fileName, - fileWeight = fileInputData?.fileSizeLabel, + fileName = file?.name, + fileWeight = file?.length()?.let { fileSizeLabel(it) }, onSelectFile = { uploadState = getFileUploadState(fieldUiModel.displayName, true) fieldUiModel.invokeUiEvent(UiEventType.ADD_FILE) @@ -62,6 +60,16 @@ internal fun ProvideInputFileResource( ) } +private fun fileSizeLabel(fileSize: Long) = run { + val kb = fileSize / 1024f + val mb = kb / 1024f + if (kb < 1024f) { + "${DecimalFormat("*0").format(kb)}KB" + } else { + "${DecimalFormat("*0.##").format(mb)}MB" + } +} + private fun getFileUploadState(value: String?, isLoading: Boolean): UploadFileState { return if (isLoading && value.isNullOrEmpty()) { UploadFileState.UPLOADING diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt index 976fda619d..245606bcf0 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/MultiSelectionInputProvider.kt @@ -45,9 +45,13 @@ internal fun ProvideMultiSelectionInput( legendData = fieldUiModel.legend(), isRequired = fieldUiModel.mandatory, onItemsSelected = { - val checkedValues = it.filter { item -> item.checked }.map { checkBoxData -> - val selectedIndex = data.indexOf(checkBoxData) - codeList[selectedIndex] + val checkedValues = it.mapNotNull { checkBoxData -> + if (checkBoxData.checked) { + val selectedIndex = data.indexOfFirst { originalData -> originalData.uid == checkBoxData.uid } + codeList[selectedIndex] + } else { + null + } } intentHandler( diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt index 18857aa51c..b56bc9a091 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt @@ -7,9 +7,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.res.stringResource -import org.dhis2.commons.date.DateUtils -import org.dhis2.form.R import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -17,10 +14,7 @@ import org.dhis2.form.model.FieldUiModel import org.dhis2.form.ui.event.RecyclerViewUiEvents import org.hisp.dhis.mobile.ui.designsystem.component.DropdownInputField import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem -import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown -import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle -import java.util.Date @Composable fun ProvidePeriodSelector( @@ -30,85 +24,38 @@ fun ProvidePeriodSelector( focusRequester: FocusRequester, uiEventHandler: (RecyclerViewUiEvents) -> Unit, ) { - val currentDate = DateUtils.getInstance().getStartOfDay(Date()) - if ((fieldUiModel.periodSelector?.minDate?.after(currentDate) == true)) { - ProvideEmptyPeriodSelector( - modifier = modifier, - name = fieldUiModel.label, - inputStyle = inputStyle, + var selectedItem by remember(fieldUiModel.displayName) { + mutableStateOf( + fieldUiModel.displayName, ) - } else { - var selectedItem by remember(fieldUiModel.displayName) { - mutableStateOf( - fieldUiModel.displayName, - ) - } - - DropdownInputField( - modifier = modifier, - title = fieldUiModel.label, - state = fieldUiModel.inputState(), - inputStyle = inputStyle, - legendData = fieldUiModel.legend(), - supportingTextData = fieldUiModel.supportingText(), - isRequiredField = fieldUiModel.mandatory, - selectedItem = DropdownItem(selectedItem ?: ""), - onResetButtonClicked = { - selectedItem = null - fieldUiModel.onClear() - }, - onDropdownIconClick = { - uiEventHandler( - RecyclerViewUiEvents.SelectPeriod( - uid = fieldUiModel.uid, - title = fieldUiModel.label, - periodType = fieldUiModel.periodSelector!!.type, - minDate = fieldUiModel.periodSelector!!.minDate, - maxDate = fieldUiModel.periodSelector!!.maxDate, - ), - ) - }, - onFocusChanged = {}, - focusRequester = focusRequester, - expanded = false, - ) - } -} - -@Composable -fun ProvideEmptyPeriodSelector( - modifier: Modifier = Modifier, - name: String, - inputStyle: InputStyle, -) { - var selectedItem by remember { - mutableStateOf("") } - val options = listOf(DropdownItem(stringResource(id = R.string.no_periods))) - - InputDropDown( + DropdownInputField( modifier = modifier, - title = name, - state = InputShellState.UNFOCUSED, + title = fieldUiModel.label, + state = fieldUiModel.inputState(), inputStyle = inputStyle, - selectedItem = DropdownItem(selectedItem), + legendData = fieldUiModel.legend(), + supportingTextData = fieldUiModel.supportingText(), + isRequiredField = fieldUiModel.mandatory, + selectedItem = selectedItem?.let { DropdownItem(it) }, onResetButtonClicked = { - selectedItem = "" - }, - onItemSelected = { _, newSelectedDropdownItem -> - selectedItem = newSelectedDropdownItem.label + selectedItem = null + fieldUiModel.onClear() }, - itemCount = 1, - fetchItem = { - options[it] - }, - loadOptions = { - /*no-op*/ - }, - onSearchOption = { - /*no-op*/ + onDropdownIconClick = { + uiEventHandler( + RecyclerViewUiEvents.SelectPeriod( + uid = fieldUiModel.uid, + title = fieldUiModel.label, + periodType = fieldUiModel.periodSelector!!.type, + minDate = fieldUiModel.periodSelector!!.minDate, + maxDate = fieldUiModel.periodSelector!!.maxDate, + ), + ) }, - isRequiredField = false, + onFocusChanged = {}, + focusRequester = focusRequester, + expanded = false, ) } diff --git a/form/src/main/res/values-es/strings.xml b/form/src/main/res/values-es/strings.xml index 21452f49be..6c865a8f7b 100644 --- a/form/src/main/res/values-es/strings.xml +++ b/form/src/main/res/values-es/strings.xml @@ -22,6 +22,9 @@ La regla de programa %s está intentando asignar el valor %s que no pertenece al conjunto de opciones %s El valor del campo %s no pertenece al conjunto de opciones en %s El campo es obligatorio + Tienes algunos mensajes de aviso. + Tienes algunos mensajes de alerta.\n¿Desea marcar el formulario como Completo? + Necesario Fecha Años Meses @@ -54,9 +57,13 @@ Completar Descartar los cambios ¡Guardado! + Tiene algunos mensajes de error. \n¿Desea revisarlos? + Algunos campos tienen errores y no serán guardados. \n¿Desea revisar el formulario? Algunos campos necesitan su atención.\n¿Desea revisar el formulario? Ahora no + Guardar de todos modos Seguir editando + ¿Desea marcar este formulario como completo? Si sale toda la información del formulario será descartada. Algunos campos tienen errores y no serán guardados. \nSi sale, los cambios serán descartados. Algunos campos tienen errores y no serán guardados. @@ -93,4 +100,20 @@ Coordenadas para No quedan valores reservados. Contacte con el administrador del sistema. Fecha del incidente + Se requiere el permiso de localización para obtener coordenadas. Por favor, habilítelo desde los ajustes del dispositivo. + Abrir con + No se ha encontrado opción + Descargar + Escanear + Código QR + Código de barras + Añadir ubicación + Este formulario no tiene campos configurados + %s detalles + %s datos + Unidad Organizativa + Coordenadas + Combo de categoría + No hay opciones disponibles + Cat Combo \ No newline at end of file diff --git a/form/src/main/res/values-pt/strings.xml b/form/src/main/res/values-pt/strings.xml index 96cb837770..6a58f90012 100644 --- a/form/src/main/res/values-pt/strings.xml +++ b/form/src/main/res/values-pt/strings.xml @@ -1,6 +1,7 @@ Somente valores de 0 a 100 são permitidos + Reveja este campo Erro de formatação Somente números positivos são permitidos Este não é um número de telefone válido @@ -11,16 +12,24 @@ Este url não é válido Valor deve ser um número inteiro Não é um formato correto. + O padrão configurado %s está errado. + Não foi possível atualizar este campo. Por favor, tente novamente Erro O valor deve ser único, uma coincidência foi encontrada. Não foi salvo. Por favor verifique isto. Selecionar hora Escolha a data Escolha a unidade organizacional + A regra do programa %s está a tentar atribuir um valor %s que não pertence ao conjunto de opções %s + O valor do campo %s não pertence à opção definida em %s Este campo é obrigatório + Tem algumas mensagens de aviso. + Tem algumas mensagens de aviso.\nDeseja marcar este formulário como completo? + Obrigatório Data Anos Meses Dias + %s não pode ser analisado como uma data Poligono Latitude Longitude @@ -40,17 +49,71 @@ Inserir texto longo Inserir texto Data de Vencimento + Campo obrigatório + Latitude: + Longitude: + Não guardado + Revisão Completo Descartar mudanças Gravado! + Tem algumas mensagens de erro. Quer revê-las? + Alguns campos têm erros e não são guardados. \Deseja rever o formulário? + Alguns campos requerem a sua atenção.\nDeseja rever o formulário? + Não agora + Gravar assim mesmo Continue editando + Deseja marcar este formulário como completo? + Se sair agora, todas as informações do formulário serão descartadas. + Alguns campos têm erros e não são guardados. \Se sair agora, as suas alterações serão descartadas. + Alguns campos apresentam erros e não são guardados. + Alguns campos obrigatórios estão em falta e o formulário não pode ser guardado. + Faltam alguns campos obrigatórios e o formulário não pode ser guardado. \Se sair agora, as alterações serão eliminadas. Partilha + A permissão da câmara foi negada. \Necessita de ativá-la para usar este recurso. Fechar + Aviso de regras do programa + Existe um problema de configuração que está a provocar um ciclo nas regras. Contacte o seu administrador. Copiado Nenhuma informação para este campo + Tirar fotografia + Selecionar a partir da galeria Verifique isso! + Não mostrar novamente + o valor fornecido não é permitido para este campo + Caractere inválido 0 + Caractere inválido 1 + O valor não é verdadeiro nem falso + Este formato de data não é válido + Este formato de data e hora não é válido + Este formato de hora não é válido + O valor está vazio + O valor contém mais do que uma letra + O valor não é uma letra + O texto é demasiado longo + O valor não é nem verdadeiro, nem falso + Este campo só pode ser verdadeiro + Caractere inválido 1 + Os DataValues ​​​​não podem ser guardados utilizando estes argumentos. Use o outro. + Não são permitidos números com zero à esquerda Atributos - %s Coordenadas para o Não há mais valores reservados. Entre em contato com o administrador do sistema Data do incidente + A permissão de localização é necessária para captar as coordenadas. Por favor, ative-a nas definições do aplicativo + Abrir com + Nenhuma opção encontrada + Descarregar + Digitalizar + Código QR + Código de barras + Adicionar localização + Este formulário não tem campos configurados + %s detalhes + %s dados + Unidade organizacional + Coordenadas + Combinação de categoria + Nenhuma opção disponível + Combinação de categoria \ No newline at end of file diff --git a/form/src/main/res/values-uk/strings.xml b/form/src/main/res/values-uk/strings.xml index 7d117d3fd0..b850d6f84f 100644 --- a/form/src/main/res/values-uk/strings.xml +++ b/form/src/main/res/values-uk/strings.xml @@ -99,4 +99,4 @@ Комбінація категорій Немає доступних опцій Комбінація категорій - \ No newline at end of file + \ No newline at end of file diff --git a/form/src/main/res/values-vi/strings.xml b/form/src/main/res/values-vi/strings.xml index cdb3e8bede..d2146652b6 100644 --- a/form/src/main/res/values-vi/strings.xml +++ b/form/src/main/res/values-vi/strings.xml @@ -22,6 +22,9 @@ Luật Chương Trình %s đang cố gắng gán giá trị %s mà không thuộc Bộ Tùy Chọn %s Giá trị của trường %s không thuộc Bộ Tùy Chọn trong %s Trường bắt buộc + Bạn có một số tin nhắn cảnh báo. + Bạn có một số tin nhắn cảnh báo. \n Bạn có muốn đánh dấu biểu nhập này là hoàn tất không? + Bắt buộc Ngày Năm Tháng @@ -54,9 +57,13 @@ Hoàn tất Hủy bỏ các thay đổi Đã lưu! + Bạn có một số tin nhắn lỗi.\n Bạn có muốn xem không? + Một số trường dữ liệu có lỗi và chưa được lưu. \n Bạn có muốn xem lại biểu nhập không? Một số trường dữ liệu cần bạn chú ý.\n Bạn có muốn xem xét biểu nhập không? Không phải bây giờ + Tiếp tục Lưu Tiếp tục chỉnh sửa + Bạn có muốn đánh dấu biểu nhập này là hoàn tất không? Nếu bạn tắt bây giờ, tất cả thông tin trong biểu nhập sẽ bị hủy bỏ. Một số trường dữ liệu có lỗi và chưa được lưu.\n Nếu bạn tắt bây giờ, tất cả thay đổi sẽ bị hủy bỏ. Một số trường dữ liệu bị lỗi và chưa được lưu @@ -73,8 +80,40 @@ Chọn từ thư viện ảnh Kiểm tra cái này! Không hiển thị lại + giá trị cung cấp không phù hợp với trường này + Ký tự 0 không hợp lệ + Ký tự 1 không hợp lệ + Giá trị không phải là Đúng hoặc Sai + Định dạng ngày này không hợp lệ + Định dạng ngày và thời gian này không hợp lệ + Định dạng thời gian này không hợp lệ + Giá trị rỗng + Giá trị chứa nhiều hơn 1 ký tự + Giá trị không phải là 1 ký tự + Văn bản quá dài + Giá trị không là Đúng hoặc Sai + Trường này chỉ có thể là Đúng + Ký tự 1 không hợp lệ + Các Giá trị dữ liệu không thể được lưu bằng những đối số này. Vui lòng sử dụng đối số khác. + Các số có số 0 đứng đầu không được chấp nhận Thuộc tính - %s Tọa độ của Không có giá trị dành riêng nào nữa. Liên hệ với quản lý của bạn Ngày khởi phát + Cần quyền định vị để thu thập vị trí. Xin vui lòng mở chúng trong cài đặt ứng dụng + Mở với + Không tìm thấy tùy chọn + Tải về + Quét + Mã QR + Mã vạch + Thêm vị trí + Biểu này không có trường dữ liệu được cấu hình + Chi tiết %s + Dữ liệu %s + Đơn vị + Toạ độ + Tổ hợp phân loại + Không có tùy chọn + Tổ hợp Phân Loại \ 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 4291c0a4df..c64d2249ac 100644 --- a/form/src/main/res/values/strings.xml +++ b/form/src/main/res/values/strings.xml @@ -118,6 +118,5 @@ No options available Cat combo Storage permission is not granted.\nYou need to enable it to use this feature. - No periods available \ No newline at end of file diff --git a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt index 09fec26410..6582cf13a1 100644 --- a/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt +++ b/form/src/test/java/org/dhis2/form/data/FormRepositoryImplTest.kt @@ -37,7 +37,6 @@ import org.mockito.kotlin.atLeast import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturnConsecutively import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -290,16 +289,16 @@ class FormRepositoryImplTest { fun `Should allow to complete only uncompleted events`() { whenever( dataEntryRepository.list(), - ) doReturn Flowable.just(provideMandatoryItemList().filter { !it.mandatory }) + ) doReturn Flowable.just(provideMandatoryItemList()) whenever(dataEntryRepository.isEvent()) doReturn true whenever(formValueStore.eventState()) doReturn EventStatus.ACTIVE repository.fetchFormItems() - assertTrue(repository.runDataIntegrityCheck(false) is SuccessfulResult) + assertTrue(repository.runDataIntegrityCheck(false) is MissingMandatoryResult) assertTrue(repository.runDataIntegrityCheck(false).canComplete) whenever(formValueStore.eventState()) doReturn EventStatus.COMPLETED repository.fetchFormItems() - assertTrue(repository.runDataIntegrityCheck(false) is SuccessfulResult) + assertTrue(repository.runDataIntegrityCheck(false) is MissingMandatoryResult) assertFalse(repository.runDataIntegrityCheck(false).canComplete) } diff --git a/form/src/test/java/org/dhis2/form/data/RulesUtilsProviderImplTest.kt b/form/src/test/java/org/dhis2/form/data/RulesUtilsProviderImplTest.kt index 1a8064ebeb..e87fa45c87 100644 --- a/form/src/test/java/org/dhis2/form/data/RulesUtilsProviderImplTest.kt +++ b/form/src/test/java/org/dhis2/form/data/RulesUtilsProviderImplTest.kt @@ -593,6 +593,7 @@ class RulesUtilsProviderImplTest { assertEquals(testFieldViewModels[testingUid]!!.warning, "content data") assertTrue(result.messageOnComplete == "content data") + assertTrue(result.fieldsWithWarnings.isNotEmpty()) assertTrue(result.canComplete) } diff --git a/form/src/test/java/org/dhis2/form/ui/FormViewModelTest.kt b/form/src/test/java/org/dhis2/form/ui/FormViewModelTest.kt index 64c30dae3c..9406438b81 100644 --- a/form/src/test/java/org/dhis2/form/ui/FormViewModelTest.kt +++ b/form/src/test/java/org/dhis2/form/ui/FormViewModelTest.kt @@ -4,7 +4,8 @@ import android.content.Intent import androidx.arch.core.executor.testing.InstantTaskExecutorRule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.dhis2.commons.prefs.PreferenceProvider @@ -16,6 +17,7 @@ import org.dhis2.form.data.GeometryController import org.dhis2.form.data.MissingMandatoryResult import org.dhis2.form.data.SuccessfulResult import org.dhis2.form.model.ActionType +import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.RowAction import org.dhis2.form.model.StoreResult import org.dhis2.form.model.ValueStoreResult @@ -28,9 +30,13 @@ import org.junit.Before import org.junit.Ignore import org.junit.Rule 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.whenever +import java.time.LocalDate +import java.time.format.DateTimeFormatter @ExperimentalCoroutinesApi class FormViewModelTest { @@ -39,8 +45,9 @@ class FormViewModelTest { val instantExecutorRule = InstantTaskExecutorRule() private val repository: FormRepository = mock() + private val testingDispatcher = StandardTestDispatcher() private val dispatcher: DispatcherProvider = mock { - on { io() } doReturn Dispatchers.IO + on { io() } doReturn testingDispatcher } private val preferenceProvider: PreferenceProvider = mock() private val geometryController: GeometryController = mock() @@ -49,13 +56,14 @@ class FormViewModelTest { @Before fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) + Dispatchers.setMain(testingDispatcher) viewModel = FormViewModel( repository, dispatcher, geometryController, ) + whenever(repository.getDateFormatConfiguration()) doReturn "ddMMyyyy" } @Ignore("We need to update Kotlin version in order to test coroutines") @@ -161,4 +169,49 @@ class FormViewModelTest { assertTrue(viewModel.getUpdatedData(uiEvent).value == uiEvent.value) } + + @Test + fun `Should not save last focused item when is not allowed future dates`() = runTest { + val dateField = dateFieldNotAllowedFuture + whenever(repository.currentFocusedItem()) doReturn dateField + viewModel.previousActionItem = RowAction( + id = dateField.uid, + value = "2024-12-12", + type = ActionType.ON_FOCUS, + ) + viewModel.submitIntent(FormIntent.OnFocus("newField", null)) + advanceUntilIdle() + verify(repository).updateErrorList(any()) + } + + @Test + fun `Should save last focused item with future date when is allowed future dates`() = runTest { + val dateField = dateFieldFuture + whenever(repository.currentFocusedItem()) doReturn dateField + viewModel.previousActionItem = RowAction( + id = dateField.uid, + value = "2024-12-12", + type = ActionType.ON_FOCUS, + ) + viewModel.submitIntent(FormIntent.OnFocus("newField", null)) + advanceUntilIdle() + verify(repository).save(dateField.uid, dateField.value, null) + verify(repository).updateValueOnList(dateField.uid, dateField.value, dateField.valueType) + } + + private val futureDate: String = LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_DATE) + + private val dateFieldFuture: FieldUiModel = mock { + on { uid } doReturn "fieldUid" + on { valueType } doReturn ValueType.DATE + on { allowFutureDates } doReturn true + on { value } doReturn futureDate + } + + private val dateFieldNotAllowedFuture: FieldUiModel = mock { + on { uid } doReturn "fieldUid" + on { valueType } doReturn ValueType.DATE + on { allowFutureDates } doReturn false + on { value } doReturn futureDate + } } diff --git a/form/src/test/java/org/dhis2/form/ui/provider/FormResultDialogProviderTest.kt b/form/src/test/java/org/dhis2/form/ui/provider/FormResultDialogProviderTest.kt index 19ab6cb028..d40e34ffce 100644 --- a/form/src/test/java/org/dhis2/form/ui/provider/FormResultDialogProviderTest.kt +++ b/form/src/test/java/org/dhis2/form/ui/provider/FormResultDialogProviderTest.kt @@ -7,6 +7,7 @@ import org.dhis2.form.data.MissingMandatoryResult import org.dhis2.form.data.SuccessfulResult import org.dhis2.form.model.EventMode import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle +import org.dhis2.ui.dialogs.bottomsheet.IssueType import org.hisp.dhis.android.core.common.ValidationStrategy import org.hisp.dhis.android.core.event.EventStatus import org.junit.Assert.assertTrue @@ -98,6 +99,54 @@ class FormResultDialogProviderTest { assertTrue(noErrorsInFormModel.first.mainButton == DialogButtonStyle.CompleteButton) } + @Test + fun `Should configure to show warning on complete message`() { + val completedEventWithNoErrors = SuccessfulResult( + canComplete = true, + onCompleteMessage = "Warning on complete", + eventResultDetails = EventResultDetails( + eventStatus = EventStatus.ACTIVE, + eventMode = EventMode.CHECK, + validationStrategy = ValidationStrategy.ON_COMPLETE, + ), + ) + val noErrorsInFormModel = formResultDialogProvider.invoke( + canComplete = completedEventWithNoErrors.canComplete, + onCompleteMessage = completedEventWithNoErrors.onCompleteMessage, + errorFields = emptyList(), + emptyMandatoryFields = emptyMap(), + warningFields = emptyList(), + eventMode = completedEventWithNoErrors.eventResultDetails.eventMode ?: EventMode.NEW, + eventState = completedEventWithNoErrors.eventResultDetails.eventStatus ?: EventStatus.ACTIVE, + result = completedEventWithNoErrors, + ) + assertTrue(noErrorsInFormModel.second.first().issueType == IssueType.WARNING_ON_COMPLETE) + } + + @Test + fun `Should configure to show error on complete message`() { + val completedEventWithNoErrors = SuccessfulResult( + canComplete = false, + onCompleteMessage = "error on complete", + eventResultDetails = EventResultDetails( + eventStatus = EventStatus.ACTIVE, + eventMode = EventMode.NEW, + validationStrategy = ValidationStrategy.ON_COMPLETE, + ), + ) + val noErrorsInFormModel = formResultDialogProvider.invoke( + canComplete = completedEventWithNoErrors.canComplete, + onCompleteMessage = completedEventWithNoErrors.onCompleteMessage, + errorFields = emptyList(), + emptyMandatoryFields = emptyMap(), + warningFields = emptyList(), + eventMode = completedEventWithNoErrors.eventResultDetails.eventMode ?: EventMode.NEW, + eventState = completedEventWithNoErrors.eventResultDetails.eventStatus ?: EventStatus.ACTIVE, + result = completedEventWithNoErrors, + ) + assertTrue(noErrorsInFormModel.second.first().issueType == IssueType.ERROR_ON_COMPLETE) + } + @Test fun `Should follow validation strategy when trying to save the form with errors`() { val completedEventWithNoErrors = SuccessfulResult( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d65f0672a..6db9a9a6d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] -sdk = "34" +sdk = "35" minSdk = "21" -vCode = "138" -vName = "3.1.0.1" -gradle = "8.6.1" -kotlin = '2.0.20' +vCode = "139" +vName = "3.1.1" +gradle = "8.7.2" +kotlin = '2.0.21' hilt = '2.47' jacoco = '0.8.10' -designSystem = "0.4.0.1" -dhis2sdk = "1.11.0.1" -ruleEngine = "3.0.0" +designSystem = "0.4.1" +dhis2sdk = "1.11.1" +ruleEngine = "3.2.0" expressionParser = "1.1.0" appcompat = "1.6.1" annotation = "1.6.0" @@ -28,7 +28,6 @@ recyclerview = "1.3.1" compose = "1.5.4" composePaging = "3.3.0" composeLifecycle = "2.8.1" -composeTheme = "1.2.1" composeConstraintLayout = "1.0.1" activityCompose = "1.8.2" viewModelCompose = "2.7.0" @@ -49,13 +48,6 @@ mapboxannotation = "0.8.0" matomo = "4.1.2" sentry = "7.14.0" timber = "5.0.1" -flipper = "0.161.0" -flippernoop = "0.161.0" -soloader = "0.10.4" -flippernetwork = "0.161.0" -flipperleak = "0.161.0" -leakcannary = "2.9.1" -leakcannarynoop = "1.6.3" rxlint = "1.6" crashactivity = "2.3.0" zxing = "3.5.0" @@ -142,7 +134,6 @@ google-auth = { group = "com.google.android.gms", name = "play-services-auth", v google-auth-apiphone = { group = "com.google.android.gms", name = "play-services-auth-api-phone", version = "18.0.1" } google-autoValue = { group = "com.google.auto.value", name = "auto-value", version.ref = "autovalue" } #TODO: this should be removed google-material = { group = "com.google.android.material", name = "material", version.ref = "material" } -google-material-themeadapter = { group = "com.google.android.material", name = "compose-theme-adapter", version.ref = "composeTheme" } google-material3-themeadapter = { group = "com.google.accompanist", name = "accompanist-themeadapter-material3", version.ref = "themeAdapter" } google-gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } network-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } @@ -168,13 +159,6 @@ barcodeScanner-zxing-android = { group = "com.journeyapps", name = "zxing-androi lottie = { group = "com.airbnb.android", name = "lottie", version.ref = "lottie" } lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" } analytics-matomo = { group = "com.github.matomo-org", name = "matomo-sdk-android", version.ref = "matomo" } -analytics-flipper = { group = "com.facebook.flipper", name = "flipper", version.ref = "flipper" } -analytics-flipper-network = { group = "com.facebook.flipper", name = "flipper-network-plugin", version.ref = "flippernetwork" } -analytics-flipper-leak = { group = "com.facebook.flipper", name = "flipper-leakcanary-plugin", version.ref = "flipperleak" } -analytics-flipper-noop = { group = "com.facebook.flipper", name = "flipper-noop", version.ref = "flippernoop" } -analytics-soloader = { group = "com.facebook.soloader", name = "soloader", version.ref = "soloader" } -analytics-leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcannary" } -analytics-leakcanary-noop = { group = "com.squareup.leakcanary", name = "leakcanary-android-no-op", version.ref = "leakcannarynoop" } analytics-rxlint = { group = "nl.littlerobots.rxlint", name = "rxlint", version.ref = "rxlint" } analytics-customactivityoncrash = { group = "cat.ereza", name = "customactivityoncrash", version.ref = "crashactivity" } analytics-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } @@ -222,7 +206,7 @@ deprecated-autoValueParcel = { group = "com.ryanharter.auto.value", name = "auto kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [bundles] uicomponents-implementation = ["androidx-coreKtx", "androidx-material3", "androidx-material3-window","androidx-material3-adaptative-android", "androidx-material3-adaptative-android", "google-material", "lottie-compose", "dhis2-mobile-designsystem"] -uicomponents-api = ["dhis2-mobile-designsystem", "androidx-compose-constraintlayout", "androidx-compose-preview", "androidx-compose-ui", "google-material-themeadapter", "google-material3-themeadapter"] +uicomponents-api = ["dhis2-mobile-designsystem", "androidx-compose-constraintlayout", "androidx-compose-preview", "androidx-compose-ui", "google-material3-themeadapter"] uicomponents-debugapi = ["androidx-compose-uitooling"] uicomponents-androidtest = ["test-junit-ext"] analytics-implementation = ["androidx-cardview", "androidx-constraintlayout"] @@ -238,8 +222,6 @@ table-androidTest = ["test-compose-ui-test", "test-uiautomator", "test-junitKtx" stock-implementation = ["androidx-activity-compose", "androidx-annotation", "rx-relay", "security-openId", "androidx-preferenceKtx", "androidx-work", "androidx-workgcm", "androidx-activityKtx", "androidx-lifecycle-viewmodel-compose", "analytics-customactivityoncrash", "dagger-hilt-android"] stock-core = ["desugar"] stock-kapt = ["dagger-hilt-compiler"] -stock-debugImplementation = ["analytics-flipper", "analytics-soloader"] -stock-releaseImplementation = ["analytics-flipper-noop"] stock-test = ["test-mockitoKotlin", "test-mockitoInline", "test-archCoreTesting", "test-javafaker", "test-kotlinCoroutines"] tracker-implementation = ["androidx-lifecycle-viewmodel-compose", "androidx-activity-compose"] tracker-test = ["test-mockitoCore", "test-mockitoInline", "test-mockitoKotlin", "test-testCore", "test-archCoreTesting", "test-kotlinCoroutines"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d150bbc7a..6705f77bff 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jun 26 12:02:53 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/stock-usecase/build.gradle.kts b/stock-usecase/build.gradle.kts index 8c629ffad4..b04f1fb396 100644 --- a/stock-usecase/build.gradle.kts +++ b/stock-usecase/build.gradle.kts @@ -95,13 +95,7 @@ dependencies { testImplementation(project(":dhis_android_analytics")) coreLibraryDesugaring(libs.bundles.stock.core) kapt(libs.bundles.stock.kapt) - debugImplementation(libs.bundles.stock.debugImplementation) - releaseImplementation(libs.bundles.stock.releaseImplementation) testImplementation(libs.bundles.stock.test) - - debugImplementation(libs.analytics.flipper.network) { - exclude("com.squareup.okhttp3") - } } kapt { diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/rules/RuleValidationHelperImpl.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/rules/RuleValidationHelperImpl.kt index 6b0f6c2b3b..7061515815 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/rules/RuleValidationHelperImpl.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/services/rules/RuleValidationHelperImpl.kt @@ -2,6 +2,8 @@ package org.dhis2.android.rtsm.services.rules import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.apache.commons.lang3.math.NumberUtils import org.dhis2.android.rtsm.data.AppConfig import org.dhis2.android.rtsm.data.TransactionType @@ -58,9 +60,7 @@ class RuleValidationHelperImpl @Inject constructor( addAll( entryDataValues( entry.qty, - programStage.uid(), transaction, - entry.date, appConfig, ), ) @@ -73,8 +73,6 @@ class RuleValidationHelperImpl @Inject constructor( ruleEffect.data?.let { data -> dataValues.add( RuleDataValue( - entry.date.toRuleEngineInstant(), - programStage.uid(), ruleAction.field()!!, data, ), @@ -132,6 +130,7 @@ class RuleValidationHelperImpl @Inject constructor( programStage.name()!!, RuleEventStatus.ACTIVE, period.toRuleEngineInstant(), + period.toRuleEngineInstant(), period.toRuleEngineLocalDate(), period.toRuleEngineLocalDate(), organisationUnit, @@ -225,23 +224,28 @@ class RuleValidationHelperImpl @Inject constructor( .toFlowable().flatMapIterable { events -> events } .map { event -> RuleEvent( - event.uid(), - event.programStage()!!, + event = event.uid(), + programStage = event.programStage()!!, + programStageName = d2.programModule().programStages().uid(event.programStage()) .blockingGet()!!.name()!!, + status = if (event.status() == EventStatus.VISITED) { RuleEventStatus.ACTIVE } else { RuleEventStatus.valueOf(event.status()!!.name) }, - (event.eventDate() ?: Date()).toRuleEngineInstant(), - event.dueDate()?.toRuleEngineLocalDate(), - event.completedDate()?.toRuleEngineLocalDate(), - event.organisationUnit()!!, - d2.organisationUnitModule() + eventDate = (event.eventDate() ?: Date()).toRuleEngineInstant(), + createdDate = event.created() + ?.let { Instant.fromEpochMilliseconds(it.time) } + ?: Clock.System.now(), + dueDate = event.dueDate()?.toRuleEngineLocalDate(), + completedDate = event.completedDate()?.toRuleEngineLocalDate(), + organisationUnit = event.organisationUnit()!!, + organisationUnitCode = d2.organisationUnitModule() .organisationUnits().uid(event.organisationUnit()) .blockingGet()?.code(), - event.trackedEntityDataValues()?.toRuleDataValue( + dataValues = event.trackedEntityDataValues()?.toRuleDataValue( event, d2.dataElementModule().dataElements(), d2.programModule().programRuleVariables(), @@ -271,9 +275,7 @@ class RuleValidationHelperImpl @Inject constructor( private fun entryDataValues( qty: String?, - programStage: String, transaction: Transaction, - eventDate: Date, appConfig: AppConfig, ): List { val values = mutableListOf() @@ -284,8 +286,6 @@ class RuleValidationHelperImpl @Inject constructor( ConfigUtils.getTransactionDataElement(transaction.transactionType, appConfig) values.add( RuleDataValue( - eventDate = eventDate.toRuleEngineInstant(), - programStage = programStage, dataElement = deUid, value = qty, ), @@ -301,8 +301,6 @@ class RuleValidationHelperImpl @Inject constructor( ?.code()?.let { code -> values.add( RuleDataValue( - eventDate = eventDate.toRuleEngineInstant(), - programStage = programStage, dataElement = appConfig.distributedTo, value = code, ), diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/base/BaseActivity.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/base/BaseActivity.kt index a278d1ec97..be03eec03e 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/base/BaseActivity.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/base/BaseActivity.kt @@ -180,7 +180,7 @@ abstract class BaseActivity : AppCompatActivity() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt index d4e85b7d86..7937a332d6 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/HomeActivity.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.colorResource -import com.google.android.material.composethemeadapter.MdcTheme import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanIntentResult import com.journeyapps.barcodescanner.ScanOptions @@ -38,6 +37,8 @@ import org.dhis2.commons.sync.OnSyncNavigationListener import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.sync.SyncDialog import org.dhis2.commons.sync.SyncStatusItem +import org.dhis2.commons.ui.extensions.handleInsets +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @AndroidEntryPoint class HomeActivity : AppCompatActivity() { @@ -60,13 +61,14 @@ class HomeActivity : AppCompatActivity() { intent.getParcelableExtra(INTENT_EXTRA_APP_CONFIG) ?.let { manageStockViewModel.setConfig(it) } + handleInsets() setContent { val settingsUiState by viewModel.settingsUiState.collectAsState() val helperText by viewModel.helperText.collectAsState() manageStockViewModel.setHelperText(helperText) updateTheme(settingsUiState.selectedTransactionItem.type) manageStockViewModel.setThemeColor(Color(colorResource(themeColor).toArgb())) - MdcTheme { + DHIS2Theme { Surface( modifier = Modifier.fillMaxSize(), color = Color(colorResource(themeColor).toArgb()), diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt index 8761707717..b23e5458cd 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt @@ -32,7 +32,7 @@ import org.dhis2.android.rtsm.ui.home.HomeViewModel import org.dhis2.android.rtsm.ui.home.screens.components.Backdrop import org.dhis2.android.rtsm.ui.home.screens.components.CompletionDialog import org.dhis2.android.rtsm.ui.managestock.ManageStockViewModel -import org.dhis2.ui.buttons.FAButton +import org.hisp.dhis.mobile.ui.designsystem.component.ExtendedFAB import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @@ -73,10 +73,8 @@ fun HomeScreen( enter = fadeIn(), exit = fadeOut(), ) { - FAButton( - text = dataEntryUiState.button.text, - contentColor = dataEntryUiState.button.contentColor, - containerColor = dataEntryUiState.button.containerColor, + ExtendedFAB( + text = stringResource(dataEntryUiState.button.text), icon = { Icon( painter = painterResource(id = dataEntryUiState.button.icon), @@ -84,9 +82,10 @@ fun HomeScreen( tint = dataEntryUiState.button.contentColor, ) }, - ) { - proceedAction(scope, scaffoldState) - } + onClick = { + proceedAction(scope, scaffoldState) + }, + ) } }, snackbarHost = { @@ -111,9 +110,8 @@ fun HomeScreen( label = "HomeScreenContent", ) { targetIndex -> when (targetIndex) { - BottomNavigation.ANALYTICS.id -> - { - DHIS2Theme() {} + BottomNavigation.ANALYTICS.id -> { + DHIS2Theme() { AnalyticsScreen( viewModel = viewModel, backAction = { manageStockViewModel.onHandleBackNavigation() }, @@ -123,6 +121,8 @@ fun HomeScreen( supportFragmentManager = supportFragmentManager, ) } + } + BottomNavigation.DATA_ENTRY.id -> { Backdrop( activity = activity, diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt index 215d3ef1e4..7584fcdc45 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/BackdropComponent.kt @@ -7,11 +7,11 @@ import androidx.activity.result.ActivityResultLauncher import androidx.compose.material.BackdropScaffold import androidx.compose.material.BackdropValue import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme import androidx.compose.material.ScaffoldState import androidx.compose.material.SnackbarDuration import androidx.compose.material.SnackbarResult import androidx.compose.material.rememberBackdropScaffoldState +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -162,11 +162,11 @@ private fun getScrimColor(settingsUiState: SettingsUiState): Color { if (settingsUiState.hasFacilitySelected() && settingsUiState.hasDestinationSelected()) { Color.Unspecified } else { - MaterialTheme.colors.surface.copy(alpha = 0.60f) + MaterialTheme.colorScheme.surface.copy(alpha = 0.60f) } } else { if (!settingsUiState.hasFacilitySelected()) { - MaterialTheme.colors.surface.copy(alpha = 0.60f) + MaterialTheme.colorScheme.surface.copy(alpha = 0.60f) } else { Color.Unspecified } diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/CompletionDialog.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/CompletionDialog.kt index 98173f2edb..d6af638611 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/CompletionDialog.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/CompletionDialog.kt @@ -9,9 +9,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.Snackbar import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt index 71dce8436f..e5121b3a07 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/DropdownComponents.kt @@ -18,10 +18,10 @@ import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/Toolbar.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/Toolbar.kt index df7fd63293..97e79a0401 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/Toolbar.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/components/Toolbar.kt @@ -10,12 +10,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.BackdropScaffoldState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.ScaffoldState import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -62,7 +62,7 @@ fun Toolbar( text = capitalizeText(title).ifBlank { stringResource(R.string.title_activity_home) }, - style = MaterialTheme.typography.subtitle1, + style = MaterialTheme.typography.titleMedium, maxLines = 1, fontSize = 17.sp, lineHeight = 24.sp, @@ -73,7 +73,7 @@ fun Toolbar( ) { Text( text = from, - style = MaterialTheme.typography.subtitle2, + style = MaterialTheme.typography.titleSmall, overflow = TextOverflow.Ellipsis, maxLines = 1, fontSize = 12.sp, @@ -153,7 +153,7 @@ fun ColumnScope.ProvideToolBarIcons(to: String?, hasFacilitySelected: Boolean, h ) Text( text = to, - style = MaterialTheme.typography.subtitle2, + style = MaterialTheme.typography.titleSmall, overflow = TextOverflow.Ellipsis, maxLines = 1, fontSize = 12.sp, @@ -193,7 +193,7 @@ fun AnalyticsTopBar( text = capitalizeText(title).ifBlank { stringResource(R.string.title_activity_home) }, - style = MaterialTheme.typography.subtitle1, + style = MaterialTheme.typography.titleMedium, maxLines = 1, fontSize = 17.sp, lineHeight = 24.sp, 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 d7b70a3461..fbf9e99bfc 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 @@ -50,6 +50,7 @@ import org.dhis2.composetable.model.TableCell import org.dhis2.composetable.model.TextInputModel import org.dhis2.composetable.model.ValidationResult import org.hisp.dhis.android.core.program.ProgramRuleActionType +import org.hisp.dhis.mobile.ui.designsystem.component.model.RegExValidations import org.hisp.dhis.rules.models.RuleEffect import org.jetbrains.annotations.NotNull import java.util.Collections @@ -329,9 +330,17 @@ class ManageStockViewModel @Inject constructor( currentValue = cell.value, keyboardInputType = KeyboardInputType.NumberPassword(), error = stockEntry?.errorMessage, + regex = getRegExBasedOnTransactionType(), ) } + private fun getRegExBasedOnTransactionType(): Regex? { + return when (transaction.value?.transactionType) { + TransactionType.CORRECTION -> null + else -> RegExValidations.POSITIVE_INTEGER.regex + } + } + fun onSaveValueChange(cell: TableCell) { viewModelScope.launch( dispatcherProvider.io(), diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/components/ManageStockTable.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/components/ManageStockTable.kt index 158d7f3a7f..8f134489bd 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/components/ManageStockTable.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/components/ManageStockTable.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.android.rtsm.R import org.dhis2.android.rtsm.ui.home.model.DataEntryStep import org.dhis2.android.rtsm.ui.managestock.ManageStockViewModel @@ -33,6 +32,7 @@ import org.dhis2.composetable.ui.TableConfiguration import org.dhis2.composetable.ui.TableDimensions import org.dhis2.composetable.ui.TableTheme import org.dhis2.composetable.ui.semantics.MAX_CELL_WIDTH_SPACE +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import kotlin.math.roundToInt @Composable @@ -47,7 +47,7 @@ fun ManageStockTable( ), ) - MdcTheme { + DHIS2Theme { if (viewModel.hasData.collectAsState().value) { val localDensity = LocalDensity.current val conf = LocalConfiguration.current @@ -146,6 +146,7 @@ fun ManageStockTable( tableDimensions = dimensions, tableConfiguration = TableConfiguration( headerActionsEnabled = false, + textInputViewMode = false, ), tableValidator = viewModel, tableResizeActions = tableResizeActions, diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/utils/DebugUtils.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/utils/DebugUtils.kt index 02ef7fd640..7d248c38df 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/utils/DebugUtils.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/utils/DebugUtils.kt @@ -97,7 +97,7 @@ fun printRuleEffects( dataValues?.forEach { printRuleEngineData( buffer, - " DE = ${it.dataElement}, value = ${it.value}, eventDate= ${it.eventDate}", + " DE = ${it.dataElement}, value = ${it.value}", ) } printSeparator(buffer) diff --git a/stock-usecase/src/main/res/values-es/strings.xml b/stock-usecase/src/main/res/values-es/strings.xml index 9bce21aff1..ef3bbd1ed5 100644 --- a/stock-usecase/src/main/res/values-es/strings.xml +++ b/stock-usecase/src/main/res/values-es/strings.xml @@ -1,36 +1,252 @@ + Gestión de Inventarios en Farmacias Inicio Revisar Configuración + Distribución Descartar + Corrección + Seleccione transacción + Fecha de la Transacción + Actividad reciente + Última sincronización: + Seleccione establecimiento + Distribuido a + Seleccione un + + + Servidor: Iniciar sesión + Inicia sesión en tu cuenta + Iniciar sesión con la cuenta del CICR + Solo para miembros del personal que no pertenecen al CICR + Solo para miembros del personal del CICR Nombre de usuario Contraseña + ¿Has olvidado tu contraseña\? + + Cargando actividades recientes… + Aún no has realizado ninguna actividad + + + Sincronizando metadatos… + No se pudo completar la sincronización de metadatos + Sincronizando datos… + No se pudo completar la sincronización de datos + ¡Sincronización de metadatos completada! + ¡Sincronizacion completa! + Proceder Revisar + Escanear código de barras + Escaneando… + Buscar elemento + Buscar... Eliminar Correcto Cancelar + Confirmación + De + Entregar a... + %s + A %s + + + Inglés + Francés + Español + Arabe + Ruso + + + Utilice el micrófono para la entrada de datos Idioma + Forzar la sincronización de datos Cerrar sesión + + Elemento + Inventario disponible + Cantidad + Elementos disponibles + + Valor positivo = \"cantidad faltante\" (existencias físicas menor al valor calculado) + + + Valor negativo = \"encontrado en existencias\" (existencias físicas mayores al valor calculado) + + + + + Se debe seleccionar un tipo de transacción y completar todos los campos antes de poder continuar + + + Debe seleccionar el establecimiento antes de poder continuar + + + Se debe completar el campo \"Distribuido a\" antes de poder continuar + + + Se debe completar el campo \"Distribuido a\" antes de poder continuar + + + ¿Estás seguro de que quieres volver a la pantalla anterior\? + Perderá los datos que acaba de ingresar si decide continuar. + + + ¡La transacción se completó exitosamente! + + + ¡La cancelación de la transacción se realizó con éxito! + + + ¿Estás seguro de que quieres forzar la sincronización\? + + ¿Estás seguro de que quieres cerrar sesión\? + + + ¿Estás seguro de que quieres eliminar permanentemente la entrada para la corrección de \"%1$s\"\? + Esta acción no se puede deshacer. + + + ¿Estás seguro de que quieres eliminar permanentemente la entrada para la distribución de \"%1$s\"\? + Esta acción no se puede deshacer. + + + ¿Estás seguro de que quieres eliminar permanentemente la entrada para descarte de \"%1$s\"\? + Esta acción no se puede deshacer. + + + + %d artículo + %d artículos + %d artículos + + No se encontraron elementos + Cargando artículos en stock … Correo-e Contraseña + Iniciar sesión o registrarse Iniciar sesión + ¡Bienvenido! + Nombre de usuario no válido + La contraseña debe tener >5 caracteres + Fallo al iniciar sesión + Sincronizar de nuevo + + + No se pudo iniciar sesión + No se puede cargar el archivo de configuración + + El identificador del programa de inventario no ha sido establecido en el archivo de configuración + + No se pueden cargar tus actividades recientes + No se pueden cargar la lista de establecimientos disponibles + + No se puede cargar la lista de destinos disponibles + + + No se ha podido cargar el elemento de datos de distribución + + + No se ha podido cargar el elemento de datos de corrección + + + No se puede cargar el elemento de datos de descarte + + + + Sincronizar información Buscar + Limpiar texto + Mostrar selector de fecha + Distribuido a + Última sincronización + Más información sobre cantidad Quitar elemento + Flecha direccional + Estado de actividades recientes + Cerrar guía de información + Confirmar + + Falta completar uno o más campos obligatorios en el archivo de configuración + + Seleccione el idioma deseado + + Reiniciar más tarde + + La aplicación necesita reiniciar para que se realicen los cambios, sin embargo, + los cambios no guardados se perderán.\n\n¿Quieres continuar con el reinicio?\? + + Confirmar reinicio + ¡Escaneo cancelado! + + Se produjo un error durante el cierre de sesión. Por favor, inténtelo de nuevo más tarde. + + Sesión cerrada exitosamente + No válido + + No se puede superar el inventario disponible + + + Un inventario en revisión no puede estar vacío. Puedes eliminar el ítem si no es deseado + + + Permiso concedido + Permiso denegado + + + + Permisos insuficientes. Solicitar +android.permission.RECORD_AUDIO + + Error de grabación de audio + + Error del lado del cliente. ¡Por favor, verifica tu conexión a Internet! + Error de red + La operación de red ha expirado + No se encontró ningún resultado de reconocimiento. + El servicio de reconocimiento está ocupado en este momento + An error occurred on the speech server + No se detectó entrada de voz + Se ha producido un error desconocido. + No se permiten entradas no numéricas: \"%s\" + + No se permite la entrada de números negativos: \"%s\" + + + No se puede cargar la configuración de OpenID + + Se produjo un error inesperado al autenticar con OpenID + + No se encontró conexión de red disponible. + + No se puede sincronizar los datos porque no hay conexión de red disponible + + + No se puede autenticar con su cuenta del CICR + + + + En caso de problemas, \ncomuníquese con el servicio de atención al cliente del CICR + + Algo ha ido mal. Por favor, compruebe que tanto la url del servidor como el usuario y contraseña son correctos y pruebe otra vez. + ¡Ups! Algo salió mal en el lado del servidor. + La URL entregada no es un servidor de DHIS2. + El usuario y/o la contraseña son incorrectas Servidor desconocido Error al obtener los resultados + Disculpe, algo inesperado salió mal. El servidor se encuentra ocupado en estos momentos. Por favor inténtelo más tarde. Este usuario ya ha iniciado sesión. La petición ya ha sido evaluada. @@ -46,6 +262,7 @@ Estás intentando iniciar sesión offline con un usuario diferente. No se ha podido actualizar. No se ha podido crear. + No tienes acceso. Falta la URL del servidor. Ingrese la URL. La URL del servidor está mal. Por favor revísela. Esta versión de Settings Web App no está soportada. @@ -60,8 +277,14 @@ Atrás Este programa no tiene conflictos Enviar + Desde el establecimiento + Al establecimiento + Inventario No guardado + La transacción no ha sido confirmada. Si sale ahora los datos ingresados serán descartados. + Esta transacción no ha sido confirmada. Si cambia la configuración, los datos ingresados serán descartados. Conteo + En revisión Error de formato Solo se permiten números positivos o cero No se permiten números con ceros a la izquierda diff --git a/stock-usecase/src/main/res/values-pt/strings.xml b/stock-usecase/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..eed4f8d114 --- /dev/null +++ b/stock-usecase/src/main/res/values-pt/strings.xml @@ -0,0 +1,287 @@ + + Gestão de stocks de farmácia + Início + Revisão + Configurações + + Distribuição + Descartar + Correção + Selecionar transação + Data da transação + Actividade recente + Data da última sincronização + Selecionar unidade sanitária + Distribuído para + Selecione um + + + Servidor: + Entrar + Iniciar sessão na sua conta + Iniciar sessão com a conta do ICRC + Apenas para membros do pessoal não pertencentes ao ICRC + Apenas para membros do ICRC + Nome de utilizador + Senha + Esqueceu-se da palavra-passe? + + Carregar actividades recentes… + Ainda não realizou nenhuma atividade + + + Sincronização de metadados… + Não foi possível concluir a sincronização de metadados + Sincronização de dados… + Não é possível concluir a sincronização de dados + Sincronização de metadados concluída! + Sincronização concluída! + + + Prosseguir + Revisão + + Ler código de barras + Digitalização… + Procurar item + Pesquisar... + Apagar + OK + Cancelar + Confirmação + + A partir de + Entrega para... + %s + Para %s + + + Inglês + Francês + Espanhol + Árabe + Russo + + + Use microfone para introdução de dados + Idioma + Forçar a sincronização de dados + Terminar sessão + + + + Item + Existências + Quantidade + artigos disponíveis + + Valor positivo = \"quantidade em falta\" (estoque físico inferior ao número calculado) + + + Valor negativo = \"encontrado em stock\" (estoque físico é superior ao valor calculado) + + + + Deve ser selecionado um tipo de transação e todos os campos devem ser preenchidos antes de poder prosseguir + + + O campo da instalação deve ser preenchido antes de prosseguir + + + O campo “Distribuído para” deve ser preenchido antes de prosseguir + + + The ‘Distributed to’ field must be filled in before proceeding + + + Tem a certeza de que pretende voltar ao ecrã anterior? +Se decidir continuar, perderá os dados que acabou de introduzir. + + + A transação foi concluída com êxito! + + + A transação foi cancelada com sucesso! + + + Tem a certeza de que pretende forçar a sincronização? + + Tem a certeza de que pretende terminar a sessão? + + + Tem a certeza de que pretende eliminar definitivamente a entrada para a correção de \"%1$s\"\? +Esta ação não pode ser anulada. + + + Tem a certeza de que pretende eliminar permanentemente a entrada para distribuição de\"%1$s\"\? + Esta ação não pode ser anulada. + + + Tem a certeza de que pretende eliminar permanentemente a entrada para eliminação de \"%1$s\"\? + Esta ação não pode ser anulada. + + + + %d item + %d artigos + %d itens + + Nenhum item encontrado + Carregar itens em stock … + E-mail + Palavra-passe + Iniciar sessão ou registar-se + Entrar + Bem-vindo! + Não é um nome de utilizador válido + A palavra-passe deve ter >5 caracteres + O início de sessão falhou + Voltar a sincronizar + + + Não é possível iniciar sessão + Não é possível carregar o ficheiro de configuração + + O ID do programa de stock não foi definido no ficheiro de configuração + + Não é possível carregar as suas actividades recentes + Não é possível carregar a lista de unidades sanitárias disponíveis + + Não foi possível carregar a lista de destinos disponíveis + + + Não foi possível carregar o elemento de dados de distribuição + + + Não foi possível carregar o elemento de dados de correção + + + Não foi possível carregar o elemento de dados descartados + + + + Informações de sincronização + Pesquisar + Limpar texto + Mostrar selector de datas + Distribuído para + Última sincronização + Mais informações sobre a quantidade + Remover item + Seta direcional + Estado da atividade recente + Fechar guia de informação + + Confirme + + Um ou mais campos obrigatórios estão em falta no ficheiro de configuração + + Selecione a língua pretendida + + Reiniciar mais tarde + + A aplicação necessita de ser reiniciada para que as alterações sejam efetuadas, no entanto, +quaisquer alterações não guardadas serão perdidas. \n\nDeseja prosseguir com a reinicialização\? + + Confirmar o reinício + Scan cancelado! + + Ocorreu um erro durante o logout. Por favor, tente mais tarde. + + Sair da sessão com sucesso + + Inválido + As existências disponíveis não podem ser excedidas + + Um stock em revisão não pode estar vazio. Pode remover o item se não desejar + + + Autorização concedida + Autorização recusada + + + + Permissões insuficientes. Pedido android.permissão.RECORD_AUDIO + + Erro de gravação de áudio + + Erro do lado do cliente. Por favor, verifique a sua ligação à internet! + + Erro de rede + O tempo de funcionamento da rede expirou + Nenhum resultado de reconhecimento coincidiu. + O serviço de reconhecimento está ocupado + Ocorreu um erro no servidor de voz + Não foi detectada qualquer entrada de voz + Ocorreu um erro desconhecido. +  Não é permitida a introdução de dados não numéricos: \"%s\" + + A introdução de números negativos não é permitida:\"%s\" + + + Não é possível carregar a configuração do OpenID + + Ocorreu um erro inesperado ao autenticar com OpenID + + Não há ligação de rede disponível + + Não é possível sincronizar dados porque não existe uma ligação de rede disponível + + + Não é possível autenticar com a sua conta ICRC + + + + Em caso de problemas, \ncontacte o serviço de atendimento do ICRC + + + + Algo correu mal. Verifique se o URL do servidor, o nome de utilizador e a palavra-passe estão correctos e tente novamente. + Ops! Algo correu mal no lado do servidor. + O URL fornecido não é um servidor DHIS2. + O nome de utilizador e/ou a palavra-passe estão incorrectos + Servidor desconhecido + Erro ao analisar resultados + Algo inesperado correu mal. + O servidor está um pouco ocupado neste momento. Por favor, tente mais tarde. + O utilizador já tem sessão iniciada. + O pedido já foi executado. + O pedido do servidor é inválido. + Criação abortada. O objeto já existe. + Inicie sessão para exportar a base de dados. + A exportação de uma base de dados encriptada não é suportada. + Está a importar uma base de dados já existente. + Termine a sessão para importar uma nova base de dados. + A versão da base de dados importada é superior à suportada. + Não há nenhum utilizador autenticado. + Não existe nenhum utilizador autenticado offline. + Está a tentar iniciar sessão offline com um utilizador diferente. + Não foi possível atualizar o objeto. + O objeto não pôde ser inserido. + Não tem acesso. + O URL do servidor está em falta. Por favor, introduza o URL. + O URL do servidor não está bem formado. Por favor, verifique-o. + Esta versão da aplicação Web de definições não é suportada. + A aplicação Web de definições não está instalada no servidor. + O servidor demorou demasiado tempo a responder. Cancelamos a requisição. + Não foi possível encontrar o URL. Por favor, verifique-o. + A sua conta de utilizador foi desactivada. Se isto for um erro, contacte o seu administrador. + A sua conta de utilizador está bloqueada. Se isto for um erro, contacte o seu administrador. + O valor não pode ser definido. + Existe um problema com os certificados do servidor. Por favor, contacte o seu administrador. + Início + Voltar + Não há conflitos para este programa + Enviar + Da unidade sanitária + Para a unidade sanitária + Stock + Não guardado + A transação não foi confirmada. Se sair agora, os dados introduzidos serão eliminados. + Esta transação não foi confirmada. Se alterar as definições, os dados introduzidos serão anulados. + Contagem + Em revisão + Erro de formatação + Somente números positivos ou zero são permitidos + Não são permitidos números com zero à esquerda + \ No newline at end of file diff --git a/stock-usecase/src/main/res/values-vi/strings.xml b/stock-usecase/src/main/res/values-vi/strings.xml index 188d03ea3f..14f38a3587 100644 --- a/stock-usecase/src/main/res/values-vi/strings.xml +++ b/stock-usecase/src/main/res/values-vi/strings.xml @@ -75,21 +75,21 @@ Số lượng Các mặt hàng có sẵn - Positive value = \"số lượng đang thiếu\" (số lượng thực tế ít hơn giá trị được tính toán) + Giá trị dương= \"số lượng thiếu\" (số lượng thực tế ít hơn giá trị được tính toán) - Negative value = \"đang có trong kho\" (số lượng thực tế nhiều hơn giá trị được tính toán) + Giá trị âm= \"đang có trong kho\" (số lượng thực tế nhiều hơn giá trị được tính toán) - Loại giao dịch phải được chọn và tất cả các trường phải được điền đầy đủ trước khi tiếp tục + Chọn Kiểu giao dịch và điền đầy đủ tất cả các trường trước khi bạn có thể tiếp tục - Trường cơ sở phải được điền đầy đủ trước khi tiếp tục + Điền đầy đủ các trường dữ liệu cơ sở trước khi bạn có thể tiếp tục - Trường \"Phân bổ đến\" phải được điền đầy đủ trước khi tiếp tục + Điền đầy đủ Trường \"Phân bổ đến\" trước khi tiếp tục Trường \"Phân bổ đến\" phải được điền đầy đủ trước khi tiếp tục diff --git a/stock-usecase/src/main/res/values-zh/strings.xml b/stock-usecase/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..ef66a31ccc --- /dev/null +++ b/stock-usecase/src/main/res/values-zh/strings.xml @@ -0,0 +1,286 @@ + + 药房库存管理 + 首页 + 审查 + 设置 + + 分配 + 放弃 + 更正 + 选择交易 + 交易日期 + 近期活动 + 上次同步: + 选择机构设施 + 分发给 + 选择一个 + + + 服务器: + 登录 + 登录到您的帐户 + 使用红十字国际委员会账户登录 + 仅适用于非红十字国际委员会工作人员 + 仅适用于红十字国际委员会工作人员 + 用户名 + 口令 + 忘记密码\? + + 正在加载最近的活动… + 您尚未执行任何活动 + + + 同步元数据… + 无法完成元数据同步 + 同步数据… + 无法完成数据同步 + 元数据同步完成! + 同步完成! + + + 处理 + 审查 + + 扫描条码 + 扫描… + 搜索项目 + 搜索… + 删除 + + 取消 + 确认 + + + 送货至…… + %s + 至 %s + + + 英语 + 法语 + 西班牙语 + 阿拉伯 + 俄语 + + + 使用麦克风输入数据 + 语言 + 强制数据同步 + 退出 + + + + 条目 + 现货 + 数量 + 可用项目 + + 正值 = \"缺失数量\" ( 实物库存小于计算值 ) + + + 负值 = \"found in stock\" ( 实物库存大于计算值 ) + + + + + 必须选择交易类型并且必须填写所有字段才能继续 + + + 必须先填写设施字段,然后才能继续 + + + 必须填写“分发给”字段才能继续 + + + 必须填写“分发给”字段才能继续 + + + 您确定要返回上一屏幕吗? + 如果您决定继续,您将丢失刚刚输入的数据。 + + + 交易成功完成! + + + 交易取消成功! + + + 您确定要强制同步吗? + + 您确定要退出吗? + + + 您确定要永久删除更正“%1$s”\的条目吗? + 此操作无法撤消。 + + + 您确定要永久删除“%1$s”\ 的分发条目吗? + 此操作无法撤消。 + + + 您确定要永久删除“%1$s”\的丢弃条目吗? + 此操作无法撤消。 + + + + %d 项目 + + 未找到任何项目 + 加载库存项目 … + 邮件 + 口令 + 登录或注册 + 登录 + 欢迎 ! + 不是有效的用户名 + 密码必须大于 5 个字符 + 登录失败 + 重新同步 + + + 无法登入 + 无法加载配置文件 + + 配置文件中没有设置股票节目id + + 无法加载您最近的活动 + 无法加载可用设施列表 + + 无法加载可用目的地列表 + + + 无法加载分布数据元素 + + + 无法加载校正数据元素 + + 无法加载丢弃的数据元素 + + + 同步信息 + 搜索 + 明文 + 显示日期选择器 + 分发给 + 上次同步 + 有关数量的更多信息 + 除去项目 + 方向箭头 + 近期活动状态 + 关闭信息指南 + + 确认 + + 配置文件中缺少一个或多个必填字段 + + 选择您想要的语言 + + 稍后重启 + + 需要重新启动应用程序才能使更改生效,但是, + 任何未保存的更改都将丢失。\n\n您要继续重启\吗? + + 确认重启 + 扫描已取消! + + 注销期间发生错误。请稍后再试。 + + 成功登出 + + 无效 + + 手头的可用库存不能超过 + + + 审核中的股票不能为空。如果不需要,您可以删除该项目 + + + 许可授予 + 没有权限 + + + + 权限不足。请求 android.permission.RECORD_AUDIO + + 录音错误 + + 客户端错误。请检查您的互联网连接! + + 网络错误 + 网络操作超时 + 没有匹配到的识别结果。 + 识别服务正忙 + 语音服务器出错 + 未检测到语音输入 + 出现未知错误。 + 不允许非数字输入:“%s” + + 不允许输入负数:“%s” + + + 无法加载 OpenID 配置 + + 使用 OpenID 进行身份验证时发生意外错误 + + 没有可用的网络连接 + + 无法同步数据,因为没有可用的网络连接 + + + 无法使用您的红十字国际委员会账户进行身份验证 + + + + 如有问题,\n请联系红十字国际委员会服务台 + + + + 出了些问题。请检查服务器 URL、用户名和密码是否正确,然后重试。 + 哎呀!服务器端出了点问题。 + 提供的 URL 不是 DHIS2 服务器。 + 用户名和/或密码不正确 + 未知服务器 + 解析结果发生错误 + 出乎意料的错误。 + 服务器此时有点忙。请稍后再试。 + 用户已经登录。 + 请求已被执行。 + 服务器请求无效。 + 创作中止。对象已经存在。 + 请登录以导出数据库。 + 不支持导出加密数据库。 + 您正在导入一个已经存在的数据库。 + 请注销以导入新数据库。 + 导入的数据库版本高于支持的版本。 + 没有经过身份验证的用户。 + 没有经过离线身份验证的用户。 + 您正在尝试使用其他用户离线登录。 + 无法更新对象。 + 无法插入对象。 + 您无权访问。 + 缺少服务器 URL。请输入网址。 + 服务器 URL 格式不正确。请检查一下。 + 不支持此版本的设置 Web 应用程序。 + 设置 Web 应用程序未安装在服务器中。 + 服务器花费了太多时间来响应。我们取消了请求。 + 我们找不到 URL。请验证一下。 + 您的用户帐户已被禁用。如果这是错误,请联系您的管理员。 + 您的用户帐户被锁定。如果这是错误,请联系您的管理员。 + 无法设置该值。 + 服务器证书有问题。请联系您的管理员。 + 首页 + 后退 + 该项目没有冲突。 + 发送 + 从设施 + 到设施 + 库存 + 未保存 + 交易尚未确认。如果您现在退出,输入的数据将被丢弃。 + 此交易尚未确认。如果更改设置,输入的数据将被丢弃。 + 计数 + 审查中 + 格式错误 + 只允许正数或零 + 不允许前导零数字 + \ No newline at end of file diff --git a/stock-usecase/src/test/java/org/dhis2/android/rtsm/services/ProgramRuleTests.kt b/stock-usecase/src/test/java/org/dhis2/android/rtsm/services/ProgramRuleTests.kt index b7b8f7bc86..9d7338d3a8 100644 --- a/stock-usecase/src/test/java/org/dhis2/android/rtsm/services/ProgramRuleTests.kt +++ b/stock-usecase/src/test/java/org/dhis2/android/rtsm/services/ProgramRuleTests.kt @@ -138,6 +138,7 @@ class ProgramRuleTests { "", RuleEventStatus.ACTIVE, Date().toRuleEngineInstant(), + Date().toRuleEngineInstant(), Date().toRuleEngineLocalDate(), null, "", @@ -145,36 +146,26 @@ class ProgramRuleTests { listOf( // PRevious Stock Balance RuleDataValue( - Date().toRuleEngineInstant(), - "", "oc8tn8CewiP", "3", ), // PSM Stock received RuleDataValue( - Date().toRuleEngineInstant(), - "", "j3ydinp6Qp8", "4", ), // PSM- Stock consumed distributed RuleDataValue( - Date().toRuleEngineInstant(), - "", "lpGYJoVUudr", "2", ), // PSM- Stock discarded RuleDataValue( - Date().toRuleEngineInstant(), - "", "I7cmT3iXT0y", "1", ), // PSM- Stock corrected RuleDataValue( - Date().toRuleEngineInstant(), - "", "ej1YwWaYGmm", "3", ), diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/EventRelationshipsRepository.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/EventRelationshipsRepository.kt index 439ed16e42..039755afa0 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/EventRelationshipsRepository.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/EventRelationshipsRepository.kt @@ -4,13 +4,17 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.dhis2.commons.resources.ResourceManager import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide import org.dhis2.tracker.relationships.model.RelationshipDirection import org.dhis2.tracker.relationships.model.RelationshipModel import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.tracker.relationships.model.RelationshipSection import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipHelper import org.hisp.dhis.android.core.relationship.RelationshipItem import org.hisp.dhis.android.core.relationship.RelationshipItemEvent import org.hisp.dhis.android.core.relationship.RelationshipType @@ -23,37 +27,68 @@ class EventRelationshipsRepository( private val profilePictureProvider: ProfilePictureProvider, ) : RelationshipsRepository(d2, resources) { - override fun getRelationshipTypes(): Flow>> { + override suspend fun getRelationshipTypes(): List { val event = d2.eventModule().events().uid(eventUid).blockingGet() val programStageUid = event?.programStage() ?: "" - val programUid = event?.program() ?: "" - return flowOf(d2.relationshipModule() - .relationshipTypes() - .withConstraints() - .byAvailableForEvent(event?.uid() ?: "") - .blockingGet().mapNotNull { relationshipType -> - val secondaryUid = when { - relationshipType.fromConstraint()?.programStage() - ?.uid() == programStageUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() - relationshipType.fromConstraint()?.program()?.uid() == programUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() + return d2.relationshipModule().relationshipService() + .getRelationshipTypesForEvents( + programStageUid = programStageUid, + ).map { relationshipWithEntitySide -> + RelationshipSection( + uid = relationshipWithEntitySide.relationshipType.uid(), + title = getRelationshipTitle( + relationshipWithEntitySide.relationshipType, + relationshipWithEntitySide.entitySide, + ), + relationships = emptyList(), + side = when (relationshipWithEntitySide.entitySide) { + RelationshipConstraintType.TO -> RelationshipConstraintSide.TO + RelationshipConstraintType.FROM -> RelationshipConstraintSide.FROM + }, + entityToAdd = when (relationshipWithEntitySide.entitySide) { + RelationshipConstraintType.FROM -> + relationshipWithEntitySide.relationshipType.toConstraint() + ?.trackedEntityType()?.uid() - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.programStage()?.uid() == programStageUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() + RelationshipConstraintType.TO -> + relationshipWithEntitySide.relationshipType.fromConstraint() + ?.trackedEntityType()?.uid() + } + ) + } + } - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.program()?.uid() == programUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() + override suspend fun getRelationshipsGroupedByTypeAndSide(relationshipSection: RelationshipSection): RelationshipSection { + val constraintType = when (relationshipSection.side) { + RelationshipConstraintSide.FROM -> RelationshipConstraintType.FROM + RelationshipConstraintSide.TO -> RelationshipConstraintType.TO + } + val relationshipType = d2.relationshipModule().relationshipTypes() + .withConstraints() + .uid(relationshipSection.uid) + .blockingGet() - else -> null - } - secondaryUid?.let { - Pair(relationshipType, secondaryUid) - } + val relationships = d2.relationshipModule().relationships() + .byItem( + RelationshipItem.builder() + .event( + RelationshipItemEvent.builder().event(eventUid).build(), + ).relationshipItemType(constraintType) + .build(), + ).byRelationshipType().eq(relationshipSection.uid) + .byDeleted().isFalse + .withItems() + .blockingGet() + .mapNotNull { relationship -> + mapToRelationshipModel( + relationship = relationship, + relationshipType = relationshipType, + eventUid = eventUid, + ) } + return relationshipSection.copy( + relationships = relationships ) } @@ -64,112 +99,118 @@ class EventRelationshipsRepository( RelationshipItemEvent.builder().event(eventUid).build(), ).build(), ).mapNotNull { relationship -> - val relationshipType = - d2.relationshipModule().relationshipTypes().withConstraints() - .uid(relationship.relationshipType()) - .blockingGet() ?: return@mapNotNull null - - val relationshipOwnerUid: String? - val direction: RelationshipDirection - if (eventUid != relationship.from()?.event()?.event()) { - relationshipOwnerUid = - relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() - direction = RelationshipDirection.FROM - } else { - relationshipOwnerUid = - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() - direction = RelationshipDirection.TO + getRelationshipTypeByUid( + relationship.relationshipType() + )?.let { type -> + mapToRelationshipModel( + relationship = relationship, + relationshipType = type, + eventUid = eventUid, + ) } - if (relationshipOwnerUid == null) return@mapNotNull null - - val event = d2.eventModule().events() - .withTrackedEntityDataValues().uid(eventUid).blockingGet() - val tei = d2.trackedEntityModule().trackedEntityInstances() - .withTrackedEntityAttributeValues().uid(relationshipOwnerUid).blockingGet() + } + ) + } - val eventDescription = event?.programStage()?.let { stage -> - getStage(stage)?.displayDescription() - } + override fun createRelationship( + selectedTeiUid: String, + relationshipTypeUid: String, + relationshipSide: RelationshipConstraintSide, + ): Relationship { + val (fromUid, toUid) = when (relationshipSide) { + RelationshipConstraintSide.FROM -> Pair(eventUid, selectedTeiUid) + RelationshipConstraintSide.TO -> Pair(selectedTeiUid, eventUid) + } + return RelationshipHelper.eventToTeiRelationship( + fromUid, toUid, relationshipTypeUid + ) + } - val (fromGeometry, toGeometry) = getGeometries( - direction = direction, - tei = tei, - event = event, - ) - val (fromValues, toValues) = getValues( - direction = direction, - relationshipOwnerUid = relationshipOwnerUid, - relationshipType = relationshipType, - relationship = relationship, - ) - val (fromProfilePic, toProfilePic) = getProfilePics( - direction = direction, - tei = tei, - ) - val (fromDefaultPic, toDefaultPic) = getDefaultPics( - direction = direction, - tei = tei, - event = event, - ) + private fun mapToRelationshipModel( + relationship: Relationship, + relationshipType: RelationshipType?, + eventUid: String, + ): RelationshipModel? { + val relationshipOwnerUid: String? + val direction: RelationshipDirection + if (eventUid != relationship.from()?.event()?.event()) { + relationshipOwnerUid = + relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() + direction = RelationshipDirection.FROM + } else { + relationshipOwnerUid = + relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() + direction = RelationshipDirection.TO + } + if (relationshipOwnerUid == null) return null - val canBeOpened = canBeOpened( - direction = direction, - tei = tei, - event = event, - ) + val event = d2.eventModule().events() + .withTrackedEntityDataValues().uid(eventUid).blockingGet() + val tei = d2.trackedEntityModule().trackedEntityInstances() + .withTrackedEntityAttributeValues().uid(relationshipOwnerUid).blockingGet() - val (fromLastUpdated, toLastUpdated) = getLastUpdatedInfo( - direction = direction, - tei = tei, - event = event, - ) + val eventDescription = event?.programStage()?.let { stage -> + getStage(stage)?.displayDescription() + } - val (fromDescription, toDescription) = getDescriptions( - direction = direction, - eventDescription = eventDescription, - ) + val (fromGeometry, toGeometry) = getGeometries( + direction = direction, + tei = tei, + event = event, + ) + val (fromValues, toValues) = getValues( + direction = direction, + relationshipOwnerUid = relationshipOwnerUid, + relationshipType = relationshipType, + relationship = relationship, + ) + val (fromProfilePic, toProfilePic) = getProfilePics( + direction = direction, + tei = tei, + ) + val (fromDefaultPic, toDefaultPic) = getDefaultPics( + direction = direction, + tei = tei, + event = event, + ) - RelationshipModel( - relationship, - fromGeometry, - toGeometry, - relationshipType, - direction, - relationshipOwnerUid, - RelationshipOwnerType.TEI, - fromValues, - toValues, - fromProfilePic, - toProfilePic, - fromDefaultPic, - toDefaultPic, - getOwnerStyle(relationshipOwnerUid, RelationshipOwnerType.TEI), - canBeOpened, - toLastUpdated, - fromLastUpdated, - toDescription, - fromDescription, - ) - } + val canBeOpened = canBeOpened( + direction = direction, + tei = tei, + event = event, ) - } - override fun getRelationshipTitle(relationshipType: RelationshipType): String { - val event = d2.eventModule().events().uid(eventUid).blockingGet() - val programStageUid = event?.programStage() ?: "" - return when (programStageUid) { - relationshipType.fromConstraint()?.programStage()?.uid() -> { - relationshipType.fromToName() ?: relationshipType.displayName() ?: "" - } + val (fromLastUpdated, toLastUpdated) = getLastUpdatedInfo( + direction = direction, + tei = tei, + event = event, + ) - relationshipType.toConstraint()?.program()?.uid() -> { - relationshipType.toFromName() ?: relationshipType.displayName() ?: "" - } + val (fromDescription, toDescription) = getDescriptions( + direction = direction, + eventDescription = eventDescription, + ) - else -> { - relationshipType.displayName() ?: "" - } - } + return RelationshipModel( + relationship, + fromGeometry, + toGeometry, + direction, + relationshipOwnerUid, + RelationshipOwnerType.TEI, + fromValues, + toValues, + fromProfilePic, + toProfilePic, + fromDefaultPic, + toDefaultPic, + getOwnerStyle(relationshipOwnerUid, RelationshipOwnerType.TEI), + canBeOpened, + toLastUpdated, + fromLastUpdated, + toDescription, + fromDescription, + ) } private fun canBeOpened( @@ -265,18 +306,18 @@ class EventRelationshipsRepository( private fun getValues( direction: RelationshipDirection, relationshipOwnerUid: String?, - relationshipType: RelationshipType, + relationshipType: RelationshipType?, relationship: Relationship ) = if (direction == RelationshipDirection.FROM) { Pair( getTeiAttributesForRelationship( relationshipOwnerUid, - relationshipType.fromConstraint(), + relationshipType?.fromConstraint(), relationship.created(), ), getEventValuesForRelationship( eventUid, - relationshipType.toConstraint(), + relationshipType?.toConstraint(), relationship.created(), ), ) @@ -284,12 +325,12 @@ class EventRelationshipsRepository( Pair( getEventValuesForRelationship( eventUid, - relationshipType.fromConstraint(), + relationshipType?.fromConstraint(), relationship.created(), ), getTeiAttributesForRelationship( relationshipOwnerUid, - relationshipType.toConstraint(), + relationshipType?.toConstraint(), relationship.created(), ), ) diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/RelationshipsRepository.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/RelationshipsRepository.kt index b1ba7ed66e..93ccfa2509 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/RelationshipsRepository.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/RelationshipsRepository.kt @@ -5,8 +5,10 @@ import org.dhis2.bindings.userFriendlyValue import org.dhis2.commons.date.toUi import org.dhis2.commons.resources.ResourceManager import org.dhis2.tracker.R +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide import org.dhis2.tracker.relationships.model.RelationshipModel import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.tracker.relationships.model.RelationshipSection import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.ObjectStyle import org.hisp.dhis.android.core.event.Event @@ -14,7 +16,9 @@ import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.program.ProgramType +import org.hisp.dhis.android.core.relationship.Relationship import org.hisp.dhis.android.core.relationship.RelationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType import org.hisp.dhis.android.core.relationship.RelationshipType import org.hisp.dhis.android.core.systeminfo.DHISVersion import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance @@ -27,9 +31,17 @@ abstract class RelationshipsRepository( private val d2: D2, private val resources: ResourceManager, ) { - abstract fun getRelationshipTypes(): Flow>> + abstract suspend fun getRelationshipTypes(): List + + abstract suspend fun getRelationshipsGroupedByTypeAndSide(relationshipSection: RelationshipSection): RelationshipSection + abstract fun getRelationships(): Flow> - abstract fun getRelationshipTitle(relationshipType: RelationshipType): String + + abstract fun createRelationship( + selectedTeiUid: String, + relationshipTypeUid: String, + relationshipSide: RelationshipConstraintSide, + ): Relationship protected fun orgUnitInScope(orgUnitUid: String?): Boolean { return orgUnitUid?.let { @@ -235,4 +247,42 @@ abstract class RelationshipsRepository( .uid(relationshipUid) .blockingDelete() } + + fun addRelationship(relationship: Relationship): Result { + return try { + val relationshipUid = d2.relationshipModule().relationships().blockingAdd(relationship) + Result.success(relationshipUid) + } catch (error: D2Error) { + Result.failure(error) + } + } + + protected fun getRelationshipTypeByUid(relationshipTypeUid: String?) = + d2.relationshipModule().relationshipTypes().withConstraints() + .uid(relationshipTypeUid) + .blockingGet() + + protected fun getRelationshipTitle( + relationshipType: RelationshipType, + entitySide: RelationshipConstraintType + ): String { + return when (entitySide) { + RelationshipConstraintType.FROM -> { + relationshipType.fromToName() ?: relationshipType.displayName() + ?: resources.getString(R.string.relationship) + } + + RelationshipConstraintType.TO -> { + relationshipType.toFromName() ?: relationshipType.displayName() + ?: resources.getString(R.string.relationship) + } + } + } + + fun hasWritePermission(relationshipTypeUid: String): Boolean { + return getRelationshipTypeByUid(relationshipTypeUid)?.let { relationshipType -> + d2.relationshipModule().relationshipService().hasAccessPermission(relationshipType) + + } ?: false + } } diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepository.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepository.kt index 9b5a5d3b1a..c3b9b922de 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepository.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepository.kt @@ -4,15 +4,21 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.dhis2.commons.resources.ResourceManager import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide import org.dhis2.tracker.relationships.model.RelationshipDirection import org.dhis2.tracker.relationships.model.RelationshipModel import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.tracker.relationships.model.RelationshipSection import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipHelper 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.trackedentity.TrackedEntityInstance import java.util.Date class TrackerRelationshipsRepository( @@ -23,29 +29,79 @@ class TrackerRelationshipsRepository( private val profilePictureProvider: ProfilePictureProvider, ) : RelationshipsRepository(d2, resources) { - override fun getRelationshipTypes(): Flow>> { - val teTypeUid = d2.trackedEntityModule().trackedEntityInstances() - .uid(teiUid) - .blockingGet()?.trackedEntityType() ?: return flowOf(emptyList()) + override suspend fun getRelationshipTypes(): List { + val tei = d2.trackedEntityModule().trackedEntityInstances() + .uid(teiUid).blockingGet() ?: return emptyList() + val programUid = d2.enrollmentModule().enrollments() + .uid(enrollmentUid).blockingGet()?.program() - return flowOf(d2.relationshipModule() - .relationshipTypes() + return d2.relationshipModule().relationshipService( ) + .getRelationshipTypesForTrackedEntities( + trackedEntityType = tei.trackedEntityType()!!, + programUid = programUid, + ).map { relationshipWithEntitySide -> + RelationshipSection( + uid = relationshipWithEntitySide.relationshipType.uid(), + title = getRelationshipTitle( + relationshipWithEntitySide.relationshipType, + relationshipWithEntitySide.entitySide, + ), + relationships = emptyList(), + side = when (relationshipWithEntitySide.entitySide) { + RelationshipConstraintType.FROM -> RelationshipConstraintSide.FROM + RelationshipConstraintType.TO -> RelationshipConstraintSide.TO + }, + entityToAdd = when (relationshipWithEntitySide.entitySide) { + RelationshipConstraintType.FROM -> + relationshipWithEntitySide.relationshipType.toConstraint() + ?.trackedEntityType()?.uid() + + RelationshipConstraintType.TO -> + relationshipWithEntitySide.relationshipType.fromConstraint() + ?.trackedEntityType()?.uid() + } + ) + } + } + + override suspend fun getRelationshipsGroupedByTypeAndSide(relationshipSection: RelationshipSection): RelationshipSection { + val tei = d2.trackedEntityModule().trackedEntityInstances() + .uid(teiUid).blockingGet() + val programUid = d2.enrollmentModule().enrollments() + .uid(enrollmentUid).blockingGet()?.program() + val constraintType = when (relationshipSection.side) { + RelationshipConstraintSide.FROM -> RelationshipConstraintType.FROM + RelationshipConstraintSide.TO -> RelationshipConstraintType.TO + } + val relationshipType = d2.relationshipModule().relationshipTypes() .withConstraints() - .byAvailableForTrackedEntityInstance(teiUid) - .blockingGet().map { relationshipType -> - val secondaryTeTypeUid = when { - relationshipType.fromConstraint()?.trackedEntityType() - ?.uid() == teTypeUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() - - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.trackedEntityType()?.uid() == teTypeUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() - - else -> null - } - Pair(relationshipType, secondaryTeTypeUid) + .uid(relationshipSection.uid) + .blockingGet() + + val relationships = d2.relationshipModule().relationships() + .byItem( + RelationshipItem.builder() + .trackedEntityInstance( + RelationshipItemTrackedEntityInstance.builder() + .trackedEntityInstance(teiUid) + .build(), + ).relationshipItemType(constraintType) + .build(), + ) + .byRelationshipType().eq(relationshipSection.uid) + .byDeleted().isFalse + .withItems() + .blockingGet() + .mapNotNull { relationship -> + mapToRelationshipModel( + relationship = relationship, + relationshipType = relationshipType, + tei = tei, + programUid = programUid, + ) } + return relationshipSection.copy( + relationships = relationships ) } @@ -64,190 +120,192 @@ class TrackerRelationshipsRepository( ).build(), ).mapNotNull { relationship -> //maps each relationship to a model - - //Gets the relationship type - val relationshipType = getRelationshipTypeByUid( + getRelationshipTypeByUid( relationship.relationshipType() - ) ?: return@mapNotNull null - val direction: RelationshipDirection - val relationshipOwnerUid: String? - val relationshipOwnerType: RelationshipOwnerType? - val fromGeometry: Geometry? - val toGeometry: Geometry? - val fromValues: List> - val toValues: List> - val fromProfilePic: String? - val toProfilePic: String? - val fromDefaultPicRes: Int - val toDefaultPicRes: Int - val canBoOpened: Boolean - val toLastUpdated: Date? - val fromLastUpdated: Date? - val toDescription: String? - val fromDescription: String? - - //Here checks if the TEI is the from or to of the relationship - when (teiUid) { - relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() -> { - direction = RelationshipDirection.TO - fromGeometry = tei?.geometry() - fromValues = getTeiAttributesForRelationship( - teiUid, - relationshipType.fromConstraint(), - relationship.created() - ) - fromProfilePic = tei?.let { profilePictureProvider(it, programUid) } - fromDefaultPicRes = getTeiDefaultRes(tei) - fromLastUpdated = tei?.lastUpdated() - fromDescription = null - // If the relationship is to a TEI then the owner is the TEI - if (relationship.to()?.trackedEntityInstance() != null) { - relationshipOwnerType = RelationshipOwnerType.TEI - relationshipOwnerUid = - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() - val toTei = d2.trackedEntityModule().trackedEntityInstances() - .uid(relationshipOwnerUid).blockingGet() - toGeometry = toTei?.geometry() - toValues = getTeiAttributesForRelationship( - toTei?.uid(), - relationshipType.toConstraint(), - relationship.created() - ) - toProfilePic = toTei?.let { profilePictureProvider(it, programUid) } - toDefaultPicRes = getTeiDefaultRes(toTei) - canBoOpened = toTei?.syncState() != State.RELATIONSHIP && - orgUnitInScope(toTei?.organisationUnit()) - toLastUpdated = toTei?.lastUpdated() - toDescription = null - } else { - // If the relationship is not to a TEI then the owner is the event - relationshipOwnerType = RelationshipOwnerType.EVENT - relationshipOwnerUid = - relationship.to()?.event()?.event() - val toEvent = d2.eventModule().events() - .uid(relationshipOwnerUid).blockingGet() - toGeometry = toEvent?.geometry() - toValues = getEventValuesForRelationship( - toEvent?.uid(), - relationshipType.toConstraint(), - relationship.created(), - ) - toProfilePic = "" - toDefaultPicRes = getEventDefaultRes(toEvent) - canBoOpened = toEvent?.syncState() != State.RELATIONSHIP && - orgUnitInScope(toEvent?.organisationUnit()) - toLastUpdated = toEvent?.lastUpdated() - toDescription = toEvent?.programStage()?.let { stage -> - getStage(stage)?.displayDescription() - } - } - } - - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() -> { - direction = RelationshipDirection.FROM - toGeometry = tei?.geometry() - toValues = getTeiAttributesForRelationship( - teiUid, - relationshipType.toConstraint(), - relationship.created(), - ) - toProfilePic = tei?.let { profilePictureProvider(it, programUid) } - toDefaultPicRes = getTeiDefaultRes(tei) - toLastUpdated = tei?.lastUpdated() - toDescription = null - if (relationship.from()?.trackedEntityInstance() != null) { - relationshipOwnerType = RelationshipOwnerType.TEI - relationshipOwnerUid = - relationship.from()?.trackedEntityInstance() - ?.trackedEntityInstance() - val fromTei = d2.trackedEntityModule().trackedEntityInstances() - .uid(relationshipOwnerUid).blockingGet() - fromGeometry = fromTei?.geometry() - fromValues = getTeiAttributesForRelationship( - fromTei?.uid(), - relationshipType.fromConstraint(), - relationship.created(), - ) - fromProfilePic = fromTei?.let { profilePictureProvider(it, programUid) } - fromDefaultPicRes = getTeiDefaultRes(fromTei) - canBoOpened = fromTei?.syncState() != State.RELATIONSHIP && - orgUnitInScope(fromTei?.organisationUnit()) - fromLastUpdated = fromTei?.lastUpdated() - fromDescription = null - } else { - relationshipOwnerType = RelationshipOwnerType.EVENT - relationshipOwnerUid = - relationship.from()?.event()?.event() - val fromEvent = d2.eventModule().events() - .uid(relationshipOwnerUid).blockingGet() - fromGeometry = fromEvent?.geometry() - fromValues = getEventValuesForRelationship( - fromEvent?.uid(), - relationshipType.fromConstraint(), - relationship.created(), - ) - fromProfilePic = "" - fromDefaultPicRes = getEventDefaultRes(fromEvent) - canBoOpened = fromEvent?.syncState() != State.RELATIONSHIP && - orgUnitInScope(fromEvent?.organisationUnit()) - fromLastUpdated = fromEvent?.lastUpdated() - fromDescription = fromEvent?.programStage()?.let { stage -> - getStage(stage)?.displayDescription() - } - } - } - - else -> return@mapNotNull null + )?.let { type -> + mapToRelationshipModel( + relationship = relationship, + relationshipType = type, + tei = tei, + programUid = programUid, + ) } - - if (relationshipOwnerUid == null) return@mapNotNull null - - RelationshipModel( - relationship, - fromGeometry, - toGeometry, - relationshipType, - direction, - relationshipOwnerUid, - relationshipOwnerType, - fromValues, - toValues, - fromProfilePic, - toProfilePic, - fromDefaultPicRes, - toDefaultPicRes, - getOwnerStyle(relationshipOwnerUid, relationshipOwnerType), - canBoOpened, - toLastUpdated, - fromLastUpdated, - toDescription, - fromDescription, - ) } ) } - override fun getRelationshipTitle(relationshipType: RelationshipType): String { - val teTypeUid = d2.trackedEntityModule().trackedEntityInstances() - .uid(teiUid) - .blockingGet()?.trackedEntityType() - return when (teTypeUid) { - relationshipType.fromConstraint()?.trackedEntityType()?.uid() -> { - relationshipType.fromToName() ?: relationshipType.displayName() ?: "" - } + override fun createRelationship( + selectedTeiUid: String, + relationshipTypeUid: String, + relationshipSide: RelationshipConstraintSide, + ): Relationship { + val (fromUid, toUid) = when (relationshipSide) { + RelationshipConstraintSide.FROM -> Pair(teiUid, selectedTeiUid) + RelationshipConstraintSide.TO -> Pair(selectedTeiUid, teiUid) + } + return RelationshipHelper.teiToTeiRelationship( + fromUid, toUid, relationshipTypeUid + ) + } + + private fun mapToRelationshipModel( + relationship: Relationship, + relationshipType: RelationshipType?, + tei: TrackedEntityInstance?, + programUid: String? + ): RelationshipModel? { + val direction: RelationshipDirection + val relationshipOwnerUid: String? + val relationshipOwnerType: RelationshipOwnerType? + val fromGeometry: Geometry? + val toGeometry: Geometry? + val fromValues: List> + val toValues: List> + val fromProfilePic: String? + val toProfilePic: String? + val fromDefaultPicRes: Int + val toDefaultPicRes: Int + val canBoOpened: Boolean + val toLastUpdated: Date? + val fromLastUpdated: Date? + val toDescription: String? + val fromDescription: String? - relationshipType.toConstraint()?.trackedEntityType()?.uid() -> { - relationshipType.toFromName() ?: relationshipType.displayName() ?: "" + //Here checks if the TEI is the from or to of the relationship + when (teiUid) { + relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() -> { + direction = RelationshipDirection.TO + fromGeometry = tei?.geometry() + fromValues = getTeiAttributesForRelationship( + teiUid, + relationshipType?.fromConstraint(), + relationship.created() + ) + fromProfilePic = tei?.let { profilePictureProvider(it, programUid) } + fromDefaultPicRes = getTeiDefaultRes(tei) + fromLastUpdated = tei?.lastUpdated() + fromDescription = null + // If the relationship is to a TEI then the owner is the TEI + if (relationship.to()?.trackedEntityInstance() != null) { + relationshipOwnerType = RelationshipOwnerType.TEI + relationshipOwnerUid = + relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() + val toTei = d2.trackedEntityModule().trackedEntityInstances() + .uid(relationshipOwnerUid).blockingGet() + toGeometry = toTei?.geometry() + toValues = getTeiAttributesForRelationship( + toTei?.uid(), + relationshipType?.toConstraint(), + relationship.created() + ) + toProfilePic = toTei?.let { profilePictureProvider(it, programUid) } + toDefaultPicRes = getTeiDefaultRes(toTei) + canBoOpened = toTei?.syncState() != State.RELATIONSHIP && + orgUnitInScope(toTei?.organisationUnit()) + toLastUpdated = toTei?.lastUpdated() + toDescription = null + } else { + // If the relationship is not to a TEI then the owner is the event + relationshipOwnerType = RelationshipOwnerType.EVENT + relationshipOwnerUid = + relationship.to()?.event()?.event() + val toEvent = d2.eventModule().events() + .uid(relationshipOwnerUid).blockingGet() + toGeometry = toEvent?.geometry() + toValues = getEventValuesForRelationship( + toEvent?.uid(), + relationshipType?.toConstraint(), + relationship.created(), + ) + toProfilePic = "" + toDefaultPicRes = getEventDefaultRes(toEvent) + canBoOpened = toEvent?.syncState() != State.RELATIONSHIP && + orgUnitInScope(toEvent?.organisationUnit()) + toLastUpdated = toEvent?.lastUpdated() + toDescription = toEvent?.programStage()?.let { stage -> + getStage(stage)?.displayDescription() + } + } } - else -> { - relationshipType.displayName() ?: "" + relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() -> { + direction = RelationshipDirection.FROM + toGeometry = tei?.geometry() + toValues = getTeiAttributesForRelationship( + teiUid, + relationshipType?.toConstraint(), + relationship.created(), + ) + toProfilePic = tei?.let { profilePictureProvider(it, programUid) } + toDefaultPicRes = getTeiDefaultRes(tei) + toLastUpdated = tei?.lastUpdated() + toDescription = null + if (relationship.from()?.trackedEntityInstance() != null) { + relationshipOwnerType = RelationshipOwnerType.TEI + relationshipOwnerUid = + relationship.from()?.trackedEntityInstance() + ?.trackedEntityInstance() + val fromTei = d2.trackedEntityModule().trackedEntityInstances() + .uid(relationshipOwnerUid).blockingGet() + fromGeometry = fromTei?.geometry() + fromValues = getTeiAttributesForRelationship( + fromTei?.uid(), + relationshipType?.fromConstraint(), + relationship.created(), + ) + fromProfilePic = fromTei?.let { profilePictureProvider(it, programUid) } + fromDefaultPicRes = getTeiDefaultRes(fromTei) + canBoOpened = fromTei?.syncState() != State.RELATIONSHIP && + orgUnitInScope(fromTei?.organisationUnit()) + fromLastUpdated = fromTei?.lastUpdated() + fromDescription = null + } else { + relationshipOwnerType = RelationshipOwnerType.EVENT + relationshipOwnerUid = + relationship.from()?.event()?.event() + val fromEvent = d2.eventModule().events() + .uid(relationshipOwnerUid).blockingGet() + fromGeometry = fromEvent?.geometry() + fromValues = getEventValuesForRelationship( + fromEvent?.uid(), + relationshipType?.fromConstraint(), + relationship.created(), + ) + fromProfilePic = "" + fromDefaultPicRes = getEventDefaultRes(fromEvent) + canBoOpened = fromEvent?.syncState() != State.RELATIONSHIP && + orgUnitInScope(fromEvent?.organisationUnit()) + fromLastUpdated = fromEvent?.lastUpdated() + fromDescription = fromEvent?.programStage()?.let { stage -> + getStage(stage)?.displayDescription() + } + } } + + else -> return null } - } - private fun getRelationshipTypeByUid(relationshipTypeUid: String?) = - d2.relationshipModule().relationshipTypes().withConstraints() - .uid(relationshipTypeUid) - .blockingGet() + if (relationshipOwnerUid == null) return null + + return RelationshipModel( + relationship, + fromGeometry, + toGeometry, + direction, + relationshipOwnerUid, + relationshipOwnerType, + fromValues, + toValues, + fromProfilePic, + toProfilePic, + fromDefaultPicRes, + toDefaultPicRes, + getOwnerStyle(relationshipOwnerUid, relationshipOwnerType), + canBoOpened, + toLastUpdated, + fromLastUpdated, + toDescription, + fromDescription, + ) + } } diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/AddRelationship.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/AddRelationship.kt new file mode 100644 index 0000000000..e79e7e3a1b --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/AddRelationship.kt @@ -0,0 +1,24 @@ +package org.dhis2.tracker.relationships.domain + +import kotlinx.coroutines.withContext +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.tracker.relationships.data.RelationshipsRepository +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide + +class AddRelationship( + private val dispatcher: DispatcherProvider, + private val repository: RelationshipsRepository, +) { + suspend operator fun invoke( + selectedTeiUid: String, + relationshipTypeUid: String, + relationshipSide: RelationshipConstraintSide, + ): Result = withContext(dispatcher.io()) { + val relationship = repository.createRelationship( + selectedTeiUid = selectedTeiUid, + relationshipTypeUid = relationshipTypeUid, + relationshipSide = relationshipSide, + ) + repository.addRelationship(relationship) + } +} diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByType.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByType.kt index b3d281527a..93da309d95 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByType.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByType.kt @@ -1,61 +1,18 @@ package org.dhis2.tracker.relationships.domain -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import org.dhis2.commons.date.DateLabelProvider +import kotlinx.coroutines.withContext +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.tracker.relationships.data.RelationshipsRepository -import org.dhis2.tracker.relationships.model.RelationshipItem -import org.dhis2.tracker.relationships.model.RelationshipModel import org.dhis2.tracker.relationships.model.RelationshipSection -import org.dhis2.tracker.ui.AvatarProvider -/* - * This use case fetches all the relationships that the tei has access to grouped by their type. - */ class GetRelationshipsByType( private val relationshipsRepository: RelationshipsRepository, - private val dateLabelProvider: DateLabelProvider, - private val avatarProvider: AvatarProvider, + private val dispatcher: DispatcherProvider, ) { - operator fun invoke(): Flow> = - relationshipsRepository.getRelationshipTypes() - .combine( - relationshipsRepository.getRelationships() - ) { types, relationships -> - types.map { type -> - val relationshipType = type.first - val teiTypeUid = type.second - - // Filter relationships once based on relationshipType - val filteredRelationships = relationships.filter { - it.relationshipType.uid() == relationshipType.uid() - } - - RelationshipSection( - title = relationshipsRepository.getRelationshipTitle(relationshipType), - relationshipType = relationshipType, - relationships = filteredRelationships.map { mapToRelationshipItem(it) }, - teiTypeUid = teiTypeUid - ) - } - } - - private fun mapToRelationshipItem(relationship: RelationshipModel): RelationshipItem { - return RelationshipItem( - uid = relationship.relationship.uid() ?: "", - title = relationship.displayRelationshipName(), - description = relationship.displayDescription(), - attributes = relationship.displayAttributes(), - ownerType = relationship.ownerType, - ownerUid = relationship.ownerUid, - avatar = avatarProvider.getAvatar( - style = relationship.ownerStyle, - profilePath = relationship.getPicturePath(), - firstAttributeValue = relationship.firstMainValue(), - ), - canOpen = relationship.canBeOpened, - lastUpdated = dateLabelProvider.span(relationship.displayLastUpdated()) - ) + suspend operator fun invoke(): List = withContext(dispatcher.io()) { + val relationshipSections = relationshipsRepository.getRelationshipTypes() + relationshipSections.map { relationshipSection -> + relationshipsRepository.getRelationshipsGroupedByTypeAndSide(relationshipSection) + } } } diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipConstraintSide.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipConstraintSide.kt new file mode 100644 index 0000000000..ccfac4e20b --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipConstraintSide.kt @@ -0,0 +1,6 @@ +package org.dhis2.tracker.relationships.model + +enum class RelationshipConstraintSide { + FROM, + TO +} diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipModel.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipModel.kt index 999bda57fd..c82ded10a2 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipModel.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipModel.kt @@ -3,14 +3,12 @@ package org.dhis2.tracker.relationships.model import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.common.ObjectStyle import org.hisp.dhis.android.core.relationship.Relationship -import org.hisp.dhis.android.core.relationship.RelationshipType import java.util.Date data class RelationshipModel( val relationship: Relationship, val fromGeometry: Geometry?, val toGeometry: Geometry?, - val relationshipType: RelationshipType, val direction: RelationshipDirection, val ownerUid: String, val ownerType: RelationshipOwnerType, diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipSection.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipSection.kt index 7eb553f4f6..5194484343 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipSection.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipSection.kt @@ -1,12 +1,9 @@ package org.dhis2.tracker.relationships.model -import org.hisp.dhis.android.core.relationship.RelationshipType - data class RelationshipSection( + val uid: String, val title: String, - val relationships: List, - val teiTypeUid: String?, - val relationshipType: RelationshipType, -) { - fun canAddRelationship(): Boolean = teiTypeUid != null -} + val relationships : List, + val side: RelationshipConstraintSide, + val entityToAdd: String?, +) diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsScreen.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsScreen.kt index 51dcad9f70..ee3e65bf1f 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsScreen.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsScreen.kt @@ -21,8 +21,13 @@ import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,13 +42,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.dhis2.tracker.R -import org.dhis2.tracker.relationships.model.ListSelectionState -import org.dhis2.tracker.relationships.model.RelationshipItem +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide import org.dhis2.tracker.relationships.model.RelationshipOwnerType -import org.dhis2.tracker.relationships.model.RelationshipSection +import org.dhis2.tracker.relationships.ui.state.ListSelectionState +import org.dhis2.tracker.relationships.ui.state.RelationshipItemUiState +import org.dhis2.tracker.relationships.ui.state.RelationshipSectionUiState +import org.dhis2.tracker.relationships.ui.state.RelationshipsUiState import org.dhis2.ui.avatar.AvatarProvider import org.dhis2.ui.avatar.AvatarProviderConfiguration -import org.hisp.dhis.android.core.relationship.RelationshipType import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell import org.hisp.dhis.mobile.ui.designsystem.component.Button @@ -64,67 +70,85 @@ import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardStat import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2TextStyle import org.hisp.dhis.mobile.ui.designsystem.theme.Shape import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import org.hisp.dhis.mobile.ui.designsystem.theme.getTextStyle @Composable fun RelationShipsScreen( - uiState: RelationshipsUiState>, + uiState: RelationshipsUiState, relationshipSelectionState: ListSelectionState, - onCreateRelationshipClick: (RelationshipSection) -> Unit, - onRelationshipClick: (RelationshipItem) -> Unit, - onRelationShipSelected: (String) -> Unit + onCreateRelationshipClick: (RelationshipSectionUiState) -> Unit, + onRelationshipClick: (RelationshipItemUiState) -> Unit, + onRelationShipSelected: (String) -> Unit, ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(color = Color.White, shape = Shape.LargeTop) - .padding(), - verticalArrangement = spacedBy(Spacing.Spacing4), - ) { - when (uiState) { - is RelationshipsUiState.Loading -> { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .height(200.dp), - contentAlignment = Alignment.Center, - ) { - ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(uiState.snackbarMessage) { + uiState.snackbarMessage.collect { message -> + snackbarHostState.showSnackbar( + message = message, + duration = SnackbarDuration.Long + ) + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { contentPadding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White, shape = Shape.LargeTop) + .padding(), + verticalArrangement = spacedBy(Spacing.Spacing4), + contentPadding = contentPadding, + ) { + when (uiState) { + is RelationshipsUiState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + } } } - } - is RelationshipsUiState.Empty, - is RelationshipsUiState.Error -> { - item { NoRelationships() } - } + is RelationshipsUiState.Empty, + is RelationshipsUiState.Error -> { + item { NoRelationships() } + } - is RelationshipsUiState.Success -> { - items(uiState.data) { item -> - RelationShipTypeSection( - title = item.title, - description = if (item.relationships.isEmpty()) { - stringResource(id = R.string.no_data) - } else { - null - }, - relationships = item.relationships, - canAddRelationship = item.canAddRelationship(), - relationshipSelectionState = relationshipSelectionState, - onCreateRelationshipClick = { - onCreateRelationshipClick(item) - }, - onRelationshipClick = { - onRelationshipClick(it) - }, - onRelationshipSelected = onRelationShipSelected, - ) + is RelationshipsUiState.Success -> { + items(uiState.data) { item -> + RelationShipTypeSection( + title = item.title, + description = if (item.relationships.isEmpty()) { + stringResource(id = R.string.no_data) + } else { + null + }, + relationships = item.relationships, + canAddRelationship = item.entityToAdd != null, + relationshipSelectionState = relationshipSelectionState, + onCreateRelationshipClick = { + onCreateRelationshipClick(item) + }, + onRelationshipClick = { + onRelationshipClick(it) + }, + onRelationshipSelected = onRelationShipSelected, + ) + } } } } } + } @Composable @@ -132,11 +156,11 @@ private fun RelationShipTypeSection( modifier: Modifier = Modifier, title: String, description: String?, - relationships: List, + relationships: List, canAddRelationship: Boolean, relationshipSelectionState: ListSelectionState, onCreateRelationshipClick: () -> Unit, - onRelationshipClick: (RelationshipItem) -> Unit, + onRelationshipClick: (RelationshipItemUiState) -> Unit, onRelationshipSelected: (String) -> Unit, ) { var expanded by remember { mutableStateOf(false) } @@ -286,14 +310,11 @@ fun NoRelationshipsPreview() { fun RelationShipScreenPreview() { val mockUiState = RelationshipsUiState.Success( data = listOf( - RelationshipSection( + RelationshipSectionUiState( + uid = "uid1", title = "Relationship type", - relationshipType = RelationshipType.builder() - .uid("") - .displayName("Relationship type") - .build(), relationships = listOf( - RelationshipItem( + RelationshipItemUiState( uid = "uidA", title = "First name: Peter", description = null, @@ -311,7 +332,7 @@ fun RelationShipScreenPreview() { canOpen = true, lastUpdated = "Yesterday", ), - RelationshipItem( + RelationshipItemUiState( uid = "uidB", title = "First name: Mario", description = null, @@ -330,16 +351,15 @@ fun RelationShipScreenPreview() { lastUpdated = "Yesterday", ) ), - teiTypeUid = null, + side = RelationshipConstraintSide.FROM, + entityToAdd = null, ), - RelationshipSection( + RelationshipSectionUiState( + uid = "uid2", title = "Empty relation ship", - relationshipType = RelationshipType.builder() - .uid("") - .displayName("Empty relation ship") - .build(), relationships = emptyList(), - teiTypeUid = "teiTypeUid", + side = RelationshipConstraintSide.FROM, + entityToAdd = null, ) ) ) @@ -378,6 +398,12 @@ fun DeleteRelationshipsConfirmation( }, buttonBlock = { ButtonBlock( + modifier = Modifier.padding( + top = Spacing24, + bottom = Spacing24, + start = Spacing24, + end = Spacing24 + ), primaryButton = { Button( style = ButtonStyle.OUTLINED, diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsUiState.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsUiState.kt deleted file mode 100644 index dc70158f9f..0000000000 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.dhis2.tracker.relationships.ui - -sealed class RelationshipsUiState { - data object Loading : RelationshipsUiState() - data object Empty : RelationshipsUiState() - data class Success(val data: T) : RelationshipsUiState() - data class Error(val message: String) : RelationshipsUiState() -} diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsViewModel.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsViewModel.kt index 407fe4ad7d..c1319f632d 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsViewModel.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/RelationshipsViewModel.kt @@ -2,50 +2,49 @@ package org.dhis2.tracker.relationships.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.dhis2.commons.resources.D2ErrorUtils import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.tracker.relationships.domain.AddRelationship import org.dhis2.tracker.relationships.domain.DeleteRelationships import org.dhis2.tracker.relationships.domain.GetRelationshipsByType -import org.dhis2.tracker.relationships.model.ListSelectionState -import org.dhis2.tracker.relationships.model.RelationshipSection +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide +import org.dhis2.tracker.relationships.ui.mapper.RelationshipsUiStateMapper +import org.dhis2.tracker.relationships.ui.state.ListSelectionState +import org.dhis2.tracker.relationships.ui.state.RelationshipsUiState -@OptIn(ExperimentalCoroutinesApi::class) class RelationshipsViewModel( + private val dispatcher: DispatcherProvider, private val getRelationshipsByType: GetRelationshipsByType, private val deleteRelationships: DeleteRelationships, - private val dispatcher: DispatcherProvider, + private val addRelationship: AddRelationship, + private val d2ErrorUtils: D2ErrorUtils, + private val relationshipsUiStateMapper: RelationshipsUiStateMapper, ) : ViewModel() { - - private val _relationshipsUiState = MutableStateFlow>>(RelationshipsUiState.Loading) - val relationshipsUiState: StateFlow>> = _relationshipsUiState.asStateFlow() + private val _relationshipsUiState = + MutableStateFlow(RelationshipsUiState.Loading) + val relationshipsUiState: StateFlow = + _relationshipsUiState.asStateFlow() private val _relationshipSelectionState = MutableStateFlow(ListSelectionState()) val relationshipSelectionState = _relationshipSelectionState.asStateFlow() private val _showDeleteConfirmation = MutableStateFlow(false) - var showDeleteConfirmation = _showDeleteConfirmation.asStateFlow() + val showDeleteConfirmation = _showDeleteConfirmation.asStateFlow() fun refreshRelationships() { - viewModelScope.launch(dispatcher.io()) { - getRelationshipsByType() - .flatMapLatest { - if (it.isEmpty()) { - flowOf(RelationshipsUiState.Empty) - } else { - flowOf(RelationshipsUiState.Success(it)) - } - } - .collect { - _relationshipsUiState.value = it - } + viewModelScope.launch { + val relationships = getRelationshipsByType() + _relationshipsUiState.value = if (relationships.isEmpty()) { + RelationshipsUiState.Empty + } else { + RelationshipsUiState.Success(relationshipsUiStateMapper.map(relationships)) + } } } @@ -97,10 +96,10 @@ class RelationshipsViewModel( fun stopSelectingMode() { viewModelScope.launch(dispatcher.io()) { _relationshipSelectionState.update { - it.copy( + it.copy( selectingMode = false, selectedItems = emptyList() - ) + ) } } } @@ -125,4 +124,33 @@ class RelationshipsViewModel( fun onDismissDelete() { _showDeleteConfirmation.value = false } -} \ No newline at end of file + + fun onAddRelationship( + selectedTeiUid: String, + relationshipTypeUid: String, + relationshipSide: RelationshipConstraintSide, + ) { + viewModelScope.launch(dispatcher.io()) { + addRelationship( + selectedTeiUid = selectedTeiUid, + relationshipTypeUid = relationshipTypeUid, + relationshipSide = relationshipSide, + ).fold( + onSuccess = { + refreshRelationships() + }, + onFailure = { d2Error -> + d2ErrorUtils.getErrorMessage(d2Error)?.let { + showSnackbar(it) + } + } + ) + } + } + + private fun showSnackbar(message: String) { + viewModelScope.launch { + _relationshipsUiState.value.sendSnackbarMessage(message) + } + } +} diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/mapper/RelationshipsUiStateMapper.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/mapper/RelationshipsUiStateMapper.kt new file mode 100644 index 0000000000..c8974f2d86 --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/mapper/RelationshipsUiStateMapper.kt @@ -0,0 +1,46 @@ +package org.dhis2.tracker.relationships.ui.mapper + +import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.tracker.relationships.model.RelationshipModel +import org.dhis2.tracker.relationships.model.RelationshipSection +import org.dhis2.tracker.relationships.ui.state.RelationshipItemUiState +import org.dhis2.tracker.relationships.ui.state.RelationshipSectionUiState +import org.dhis2.tracker.ui.AvatarProvider + +class RelationshipsUiStateMapper( + private val avatarProvider: AvatarProvider, + private val dateLabelProvider: DateLabelProvider, +) { + + fun map(relationships: List): List { + return relationships.map { relationshipType -> + RelationshipSectionUiState( + uid = relationshipType.uid, + title = relationshipType.title, + relationships = relationshipType.relationships.map { + mapToRelationshipItem(it) + }, + side = relationshipType.side, + entityToAdd = relationshipType.entityToAdd + ) + } + } + + private fun mapToRelationshipItem(relationship: RelationshipModel): RelationshipItemUiState { + return RelationshipItemUiState( + uid = relationship.relationship.uid() ?: "", + title = relationship.displayRelationshipName(), + description = relationship.displayDescription(), + attributes = relationship.displayAttributes(), + ownerType = relationship.ownerType, + ownerUid = relationship.ownerUid, + avatar = avatarProvider.getAvatar( + style = relationship.ownerStyle, + profilePath = relationship.getPicturePath(), + firstAttributeValue = relationship.firstMainValue(), + ), + canOpen = relationship.canBeOpened, + lastUpdated = dateLabelProvider.span(relationship.displayLastUpdated()) + ) + } +} \ No newline at end of file diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/ListSelectionState.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/ListSelectionState.kt similarity index 90% rename from tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/ListSelectionState.kt rename to tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/ListSelectionState.kt index a3551714f2..59f065625e 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/ListSelectionState.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/ListSelectionState.kt @@ -1,4 +1,4 @@ -package org.dhis2.tracker.relationships.model +package org.dhis2.tracker.relationships.ui.state import org.hisp.dhis.mobile.ui.designsystem.component.SelectionState diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipItem.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipItemUiState.kt similarity index 69% rename from tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipItem.kt rename to tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipItemUiState.kt index c59c5b2d5a..e8d9253286 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipItem.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipItemUiState.kt @@ -1,8 +1,9 @@ -package org.dhis2.tracker.relationships.model +package org.dhis2.tracker.relationships.ui.state +import org.dhis2.tracker.relationships.model.RelationshipOwnerType import org.dhis2.ui.avatar.AvatarProviderConfiguration -data class RelationshipItem( +data class RelationshipItemUiState( val uid: String, val title: String, val description: String?, diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipSectionUiState.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipSectionUiState.kt new file mode 100644 index 0000000000..3871d3d863 --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipSectionUiState.kt @@ -0,0 +1,11 @@ +package org.dhis2.tracker.relationships.ui.state + +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide + +data class RelationshipSectionUiState( + val uid: String, + val title: String, + val relationships: List, + val side: RelationshipConstraintSide, + val entityToAdd: String? +) diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipTopBarIconState.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipTopBarIconState.kt similarity index 93% rename from tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipTopBarIconState.kt rename to tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipTopBarIconState.kt index 28cb2b387d..bbecdc2042 100644 --- a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/model/RelationshipTopBarIconState.kt +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipTopBarIconState.kt @@ -1,4 +1,4 @@ -package org.dhis2.tracker.relationships.model +package org.dhis2.tracker.relationships.ui.state import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.List diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipsUiState.kt b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipsUiState.kt new file mode 100644 index 0000000000..1a2f1b3a91 --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/relationships/ui/state/RelationshipsUiState.kt @@ -0,0 +1,18 @@ +package org.dhis2.tracker.relationships.ui.state + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +sealed class RelationshipsUiState { + private val _snackbarMessage = MutableSharedFlow() + val snackbarMessage: SharedFlow get() = _snackbarMessage + + suspend fun sendSnackbarMessage(message: String) { + _snackbarMessage.emit(message) + } + + data object Loading : RelationshipsUiState() + data object Empty : RelationshipsUiState() + data class Success(val data: List) : RelationshipsUiState() + data class Error(val message: String) : RelationshipsUiState() +} diff --git a/tracker/src/main/res/values-es/strings.xml b/tracker/src/main/res/values-es/strings.xml index e518160e96..1d87b3d311 100644 --- a/tracker/src/main/res/values-es/strings.xml +++ b/tracker/src/main/res/values-es/strings.xml @@ -1,4 +1,10 @@ No hay relaciones, haga click en \"+\" para crear una nueva. - \ No newline at end of file + Relación creada el + ¿Eliminar %s\? + El %sse eliminará. Esta acción no puede deshacerse. + ¿Eliminar %drelaciones seleccionadas\? + Las %drelaciones seleccionadas se eliminarán. Esta acción no puede deshacerse. + Mostrar %dmás... + \ No newline at end of file diff --git a/tracker/src/main/res/values-pt/strings.xml b/tracker/src/main/res/values-pt/strings.xml index 70355d202f..96f020fa1b 100644 --- a/tracker/src/main/res/values-pt/strings.xml +++ b/tracker/src/main/res/values-pt/strings.xml @@ -1,4 +1,10 @@ Não há relacionamentos, clique em \"+\" para adicionar um novo. - \ No newline at end of file + Relação criada em + Remover %s\? + O ficheiro %s será eliminado. Esta ação não pode ser anulada. + Remover %d relacionamentos selecionadas\? + Os relacionamentos %d selecionadas serão eliminados. Esta acção não pode ser anulada. + Mostrar %d mais... + \ No newline at end of file diff --git a/tracker/src/main/res/values-vi/strings.xml b/tracker/src/main/res/values-vi/strings.xml index 78b7495daf..c28ec8500c 100644 --- a/tracker/src/main/res/values-vi/strings.xml +++ b/tracker/src/main/res/values-vi/strings.xml @@ -1,4 +1,8 @@ Không có mối quan hệ, bấm vào \"+\" để thêm một mối quan hệ. - \ No newline at end of file + Mối quan hệ đã được tạo trên + Bỏ %s\? + %ssẽ bị xóa. Hành động này không thể hoàn lại. + Bỏ %d mối quan hệ được chọn\? + \ No newline at end of file diff --git a/tracker/src/main/res/values/strings.xml b/tracker/src/main/res/values/strings.xml index 53478afd2c..3659326363 100644 --- a/tracker/src/main/res/values/strings.xml +++ b/tracker/src/main/res/values/strings.xml @@ -7,4 +7,5 @@ Remove %d selected relationships? The %d selected relationships will be deleted. This action cannot be undone. Show %d more... + Relationship \ No newline at end of file diff --git a/tracker/src/test/kotlin/org/dhis2/tracker/relationships/RelationshipFakeModels.kt b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/RelationshipFakeModels.kt new file mode 100644 index 0000000000..70deaf7309 --- /dev/null +++ b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/RelationshipFakeModels.kt @@ -0,0 +1,119 @@ +package org.dhis2.tracker.relationships + +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide +import org.dhis2.tracker.relationships.model.RelationshipDirection +import org.dhis2.tracker.relationships.model.RelationshipModel +import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.tracker.relationships.model.RelationshipSection +import org.hisp.dhis.android.core.common.ObjectWithUid +import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.relationship.RelationshipConstraint +import org.hisp.dhis.android.core.relationship.RelationshipType +import org.mockito.kotlin.mock +import java.util.Date + +val relationshipSection1 = RelationshipSection( + uid = "relationshipTypeUid1", + title = "RelationshipType1 FROM", + relationships = emptyList(), + side = RelationshipConstraintSide.FROM, + entityToAdd = "trackedEntityType2", +) + +val relationshipSection2 = + RelationshipSection( + uid = "relationshipTypeUid2", + title = "RelationshipType2 FROM", + relationships = emptyList(), + side = RelationshipConstraintSide.FROM, + entityToAdd = "trackedEntityType2", + ) + + val relationshipModel1 = RelationshipModel( + ownerType = RelationshipOwnerType.TEI, + ownerUid = "OwnerUid1", + canBeOpened = true, + fromValues = listOf( + "MainValue1" to "Value1", + "SecondMainValue1" to "SecondValue1", + ), + toValues = emptyList(), + fromImage = null, + toImage = null, + fromDefaultImageResource = 0, + toDefaultImageResource = 0, + ownerStyle = mock(), + fromLastUpdated = Date(), + toLastUpdated = Date(), + fromDescription = "Description 1", + toDescription = "Description 1", + fromGeometry = null, + toGeometry = null, + direction = RelationshipDirection.FROM, + relationship = Relationship.builder().uid("uid1").build() +) + + val relationshipModel2 = RelationshipModel( + ownerType = RelationshipOwnerType.TEI, + ownerUid = "OwnerUid2", + canBeOpened = true, + fromValues = listOf( + "MainValue2" to "Value2", + "SecondMainValue2" to "SecondValue2", + ), + toValues = emptyList(), + fromImage = null, + toImage = null, + fromDefaultImageResource = 0, + toDefaultImageResource = 0, + ownerStyle = mock(), + fromLastUpdated = Date(), + toLastUpdated = Date(), + fromDescription = "Description 2", + toDescription = "Description 2", + fromGeometry = null, + toGeometry = null, + direction = RelationshipDirection.FROM, + relationship = Relationship.builder().uid("uid2").build() +) + +val relationshipTypeTeiToTei = RelationshipType.builder() + .uid("relationshipTypeUid1") + .fromToName("RelationshipType1 FROM") + .toFromName("RelationshipType1 TO") + .displayName("Tei to Tei relationship") + .fromConstraint( + RelationshipConstraint.builder() + .trackedEntityType( + ObjectWithUid.create("trackedEntityType1") + ).build() + ) + .toConstraint( + RelationshipConstraint.builder() + .trackedEntityType( + ObjectWithUid.create("trackedEntityType2") + ) + .build() + ) + .build() + +val relationshipTypeEventToTei = RelationshipType.builder() + .uid("relationshipTypeUid2") + .fromToName("RelationshipType2 FROM") + .toFromName("RelationshipType2 TO") + .displayName("Event to Tei relationship") + .fromConstraint( + RelationshipConstraint.builder() + .programStage( + ObjectWithUid.create("programStageUid") + ) + .build() + ) + .toConstraint( + RelationshipConstraint.builder() + .trackedEntityType( + ObjectWithUid.create("trackedEntityType2") + ) + .build() + ) + .build() \ No newline at end of file diff --git a/tracker/src/test/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepositoryTest.kt b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepositoryTest.kt new file mode 100644 index 0000000000..3189421f9e --- /dev/null +++ b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/data/TrackerRelationshipsRepositoryTest.kt @@ -0,0 +1,104 @@ +package org.dhis2.tracker.relationships.data + +import kotlinx.coroutines.test.runTest +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.tracker.relationships.relationshipSection1 +import org.dhis2.tracker.relationships.relationshipSection2 +import org.dhis2.tracker.relationships.relationshipTypeEventToTei +import org.dhis2.tracker.relationships.relationshipTypeTeiToTei +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.relationship.RelationshipConstraintType +import org.hisp.dhis.android.core.relationship.RelationshipTypeWithEntitySide +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class TrackerRelationshipsRepositoryTest { + + private lateinit var trackerRelationshipsRepository: TrackerRelationshipsRepository + + private val resources: ResourceManager = mock() + private val profilePictureProvider: ProfilePictureProvider = mock() + + private val d2: D2 = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) + private val trackedEntityInstance: TrackedEntityInstance = mock() + private val enrollment: Enrollment = mock() + + private val teiUid = "teiUid" + private val enrollmentUid = "enrollmentUid" + private val trackedEntityType = "trackedEntityType1" + + @Before + fun setup() { + whenever( + d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .blockingGet() + ) doReturn trackedEntityInstance + whenever( + trackedEntityInstance.trackedEntityType() + ) doReturn trackedEntityType + + whenever( + d2.enrollmentModule() + .enrollments() + .uid(enrollmentUid) + .blockingGet() + ) doReturn enrollment + + trackerRelationshipsRepository = TrackerRelationshipsRepository( + d2 = d2, + resources = resources, + teiUid = teiUid, + enrollmentUid = enrollmentUid, + profilePictureProvider = profilePictureProvider + ) + } + + @Test + fun shouldGetRelationshipTypes() = runTest { + //Given + //A TEI enrolled in a program + whenever(enrollment.program()) doReturn "programUid_1" + + + //With two relationship types related + whenever( + d2.relationshipModule().relationshipService() + .getRelationshipTypesForTrackedEntities( + trackedEntityType = trackedEntityType, + programUid = "programUid_1" + ) + ) doReturn relationshipWithEntitySideList + + //When getting the relationship types + val relationshipTypes = trackerRelationshipsRepository.getRelationshipTypes() + + + //Then a relationshipSectionList is returned + val expectedResult = listOf( + relationshipSection1, + relationshipSection2, + ) + assertEquals(expectedResult, relationshipTypes) + } + + private val relationshipWithEntitySideList = listOf( + RelationshipTypeWithEntitySide( + relationshipType = relationshipTypeTeiToTei, + entitySide = RelationshipConstraintType.FROM + ), + RelationshipTypeWithEntitySide( + relationshipType = relationshipTypeEventToTei, + entitySide = RelationshipConstraintType.FROM + ) + ) +} \ No newline at end of file diff --git a/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/AddRelationshipTest.kt b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/AddRelationshipTest.kt new file mode 100644 index 0000000000..3eb3a3470e --- /dev/null +++ b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/AddRelationshipTest.kt @@ -0,0 +1,82 @@ +package org.dhis2.tracker.relationships.domain + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.tracker.relationships.data.RelationshipsRepository +import org.dhis2.tracker.relationships.model.RelationshipConstraintSide +import org.hisp.dhis.android.core.relationship.Relationship +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class AddRelationshipTest { + + private lateinit var addRelationship: AddRelationship + + private val dispatcherProvider: DispatcherProvider = mock { + on { io() } doReturn Dispatchers.Unconfined + } + private val repository: RelationshipsRepository = mock() + + @Before + fun setup() { + addRelationship = AddRelationship( + dispatcher = dispatcherProvider, + repository = repository + ) + } + + @Test + fun shouldAddRelationship() = runTest { + // Given user tries to add a relationship + val selectedTeiUid = "selectedTeiUid" + val relationshipTypeUid = "relationshipTypeUid" + val side = RelationshipConstraintSide.TO + + val relationship = Relationship.builder() + .uid("relationshipUid") + .build() + + // When + whenever( + repository.createRelationship(selectedTeiUid, relationshipTypeUid, side) + ) doReturn relationship + whenever( + repository.addRelationship(relationship) + ) doReturn Result.success("relationshipUid") + + val result = addRelationship(selectedTeiUid, relationshipTypeUid, side) + + // Then + assert(result.isSuccess) + } + + @Test + fun shouldFailWhenAddRelationship() = runTest { + // Given user tries to add a relationship + val selectedTeiUid = "selectedTeiUid" + val relationshipTypeUid = "relationshipTypeUid" + val side = RelationshipConstraintSide.TO + + val relationship = Relationship.builder() + .uid("relationshipUid") + .build() + + // When + whenever( + repository.createRelationship(selectedTeiUid, relationshipTypeUid, side) + ) doReturn relationship + whenever( + repository.addRelationship(relationship) + ) doReturn Result.failure(Exception("Failed to add relationship")) + + val result = addRelationship(selectedTeiUid, relationshipTypeUid, side) + + // Then there is an error when adding relationship + assert(result.isFailure) + assert(result.exceptionOrNull()?.message == "Failed to add relationship") + } +} \ No newline at end of file diff --git a/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByTypeTest.kt b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByTypeTest.kt index d3e69639f7..05d871f2e5 100644 --- a/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByTypeTest.kt +++ b/tracker/src/test/kotlin/org/dhis2/tracker/relationships/domain/GetRelationshipsByTypeTest.kt @@ -1,199 +1,76 @@ package org.dhis2.tracker.relationships.domain -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest -import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.tracker.relationships.data.RelationshipsRepository -import org.dhis2.tracker.relationships.model.RelationshipDirection -import org.dhis2.tracker.relationships.model.RelationshipItem -import org.dhis2.tracker.relationships.model.RelationshipModel -import org.dhis2.tracker.relationships.model.RelationshipOwnerType import org.dhis2.tracker.relationships.model.RelationshipSection -import org.dhis2.tracker.ui.AvatarProvider -import org.dhis2.ui.avatar.AvatarProviderConfiguration -import org.hisp.dhis.android.core.relationship.Relationship -import org.hisp.dhis.android.core.relationship.RelationshipType +import org.dhis2.tracker.relationships.relationshipModel1 +import org.dhis2.tracker.relationships.relationshipModel2 +import org.dhis2.tracker.relationships.relationshipSection1 +import org.dhis2.tracker.relationships.relationshipSection2 import org.junit.Assert.assertEquals 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.times -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.Date class GetRelationshipsByTypeTest { - lateinit var getRelationshipsByType: GetRelationshipsByType + private lateinit var getRelationshipsByType: GetRelationshipsByType private val relationshipsRepository: RelationshipsRepository = mock() - private val dateLabelProvider: DateLabelProvider = mock() - private val avatarProvider: AvatarProvider = mock { - on { getAvatar(any(), any(), any()) } doReturn AvatarProviderConfiguration.MainValueLabel( - firstMainValue = "M" - ) - } - private val relationshipType1: RelationshipType = mock { - on { uid() } doReturn "type1" - on { displayName() } doReturn "Relationship 1" - } - private val relationshipType2: RelationshipType = mock { - on { uid() } doReturn "type2" - on { displayName() } doReturn "Relationship 2" - } - private val relationship1: Relationship = mock { - on { uid() } doReturn "uid1" - } - private val relationship2: Relationship = mock { - on { uid() } doReturn "uid2" + private val dispatcherProvider: DispatcherProvider = mock { + on { io() } doReturn Dispatchers.Unconfined } @Before fun setup() { getRelationshipsByType = GetRelationshipsByType( relationshipsRepository = relationshipsRepository, - dateLabelProvider = dateLabelProvider, - avatarProvider = avatarProvider + dispatcher = dispatcherProvider, ) } @Test fun `invoke should return relationship sections grouped by type`() = runTest { - //Given a list of relationship types and relationships - whenever(relationshipsRepository.getRelationshipTypes()) doReturn getRelationshipTypesMock() - whenever(relationshipsRepository.getRelationships()) doReturn getRelationshipsMock() - whenever(relationshipsRepository.getRelationshipTitle(relationshipType1)) doReturn "Relationship 1" - whenever(relationshipsRepository.getRelationshipTitle(relationshipType2)) doReturn "Relationship 2" - whenever(dateLabelProvider.span(any())) doReturn "5 days ago" - - // When calling the use case to get the relationships grouped by type - val result = getRelationshipsByType().first() - - // Then a list of RelationshipSections should be returned - assertEquals(relationshipSections, result) - - // And verify that the repository and dateLabelProvider were called - verify(relationshipsRepository).getRelationshipTypes() - verify(relationshipsRepository).getRelationships() - verify(dateLabelProvider, times(2)).span(any()) - } - - private fun getRelationshipTypesMock(): Flow>> { - return flowOf( - listOf( - relationshipType1 to "teiType1", - relationshipType2 to "teiType2" - ) + //Given a list of relationship types and relationships + whenever(relationshipsRepository.getRelationshipTypes()) doReturn getRelationshipSectionsMock() + whenever( + relationshipsRepository.getRelationshipsGroupedByTypeAndSide(relationshipSection1) + ) doReturn relationshipSection1.copy( + relationships = listOf(relationshipModel1) ) - } - private fun getRelationshipsMock(): Flow> { - val relationshipModel1 = RelationshipModel( - relationshipType = relationshipType1, - ownerType = RelationshipOwnerType.TEI, - ownerUid = "OwnerUid1", - canBeOpened = true, - fromValues = listOf( - "MainValue1" to "Value1", - "SecondMainValue1" to "SecondValue1", - ), - toValues = emptyList(), - fromImage = null, - toImage = null, - fromDefaultImageResource = 0, - toDefaultImageResource = 0, - ownerStyle = mock(), - fromLastUpdated = Date(), - toLastUpdated = Date(), - fromDescription = "Description 1", - toDescription = "Description 1", - fromGeometry = null, - toGeometry = null, - direction = RelationshipDirection.FROM, - relationship = relationship1 + whenever( + relationshipsRepository.getRelationshipsGroupedByTypeAndSide(relationshipSection2) + ) doReturn relationshipSection2.copy( + relationships = listOf(relationshipModel2) ) - val relationshipModel2 = RelationshipModel( - relationshipType = relationshipType2, - ownerType = RelationshipOwnerType.TEI, - ownerUid = "OwnerUid2", - canBeOpened = true, - fromValues = listOf( - "MainValue2" to "Value2", - "SecondMainValue2" to "SecondValue2", + // When calling the use case to get the relationships grouped by type + val result = getRelationshipsByType() + + // Then a list of RelationshipSections with their relationships should be returned + val expectedResult = listOf( + relationshipSection1.copy( + relationships = listOf(relationshipModel1), ), - toValues = emptyList(), - fromImage = null, - toImage = null, - fromDefaultImageResource = 0, - toDefaultImageResource = 0, - ownerStyle = mock(), - fromLastUpdated = Date(), - toLastUpdated = Date(), - fromDescription = "Description 2", - toDescription = "Description 2", - fromGeometry = null, - toGeometry = null, - direction = RelationshipDirection.FROM, - relationship = relationship2 + relationshipSection2.copy( + relationships = listOf(relationshipModel2), + ) ) + assertEquals(expectedResult, result) - - return flowOf( - listOf(relationshipModel1, relationshipModel2) - ) } - private val relationshipSections = listOf( - RelationshipSection( - title = "Relationship 1", - relationshipType = relationshipType1, - relationships = listOf( - RelationshipItem( - uid = "uid1", - title = "MainValue1: Value1", - description = "Description 1", - attributes = listOf( - "SecondMainValue1" to "SecondValue1" - ), - ownerType = RelationshipOwnerType.TEI, - ownerUid = "OwnerUid1", - avatar = AvatarProviderConfiguration.MainValueLabel( - firstMainValue = "M", - ), - canOpen = true, - lastUpdated = "5 days ago" - ) - ), - teiTypeUid = "teiType1" - ), - RelationshipSection( - title = "Relationship 2", - relationshipType = relationshipType2, - relationships = listOf( - RelationshipItem( - uid = "uid2", - title = "MainValue2: Value2", - description = "Description 2", - attributes = listOf( - "SecondMainValue2" to "SecondValue2" - ), - ownerType = RelationshipOwnerType.TEI, - ownerUid = "OwnerUid2", - avatar = AvatarProviderConfiguration.MainValueLabel( - firstMainValue = "M", - ), - canOpen = true, - lastUpdated = "5 days ago" - ) - ), - teiTypeUid = "teiType2" + private fun getRelationshipSectionsMock(): List { + return listOf( + relationshipSection1, + relationshipSection2, ) - ) + } } \ No newline at end of file diff --git a/ui-components/src/androidTest/java/org/dhis2/ui/ExampleInstrumentedTest.kt b/ui-components/src/androidTest/java/org/dhis2/ui/ExampleInstrumentedTest.kt deleted file mode 100644 index 70e81dc505..0000000000 --- a/ui-components/src/androidTest/java/org/dhis2/ui/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.dhis2.ui - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("org.dhis2.ui.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/ui-components/src/main/java/org/dhis2/ui/MetadataIcon.kt b/ui-components/src/main/java/org/dhis2/ui/MetadataIcon.kt index 0f1e479c6e..f5f44cce73 100644 --- a/ui-components/src/main/java/org/dhis2/ui/MetadataIcon.kt +++ b/ui-components/src/main/java/org/dhis2/ui/MetadataIcon.kt @@ -21,12 +21,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.ui.theme.programColorDark import org.dhis2.ui.theme.programColorLight import org.hisp.dhis.mobile.ui.designsystem.component.ImageCardData import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatar import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @Composable fun MetadataIcon( @@ -79,7 +79,7 @@ fun ComposeView.setUpMetadataIcon( handleComposeDispose() } setContent { - MdcTheme { + DHIS2Theme { MetadataIcon(metadataIconData = metadataIconData) } } diff --git a/ui-components/src/main/java/org/dhis2/ui/Progress.kt b/ui-components/src/main/java/org/dhis2/ui/Progress.kt deleted file mode 100644 index 00bc559b87..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/Progress.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.dhis2.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.dhis2.ui.theme.textSecondary - -@Composable -fun Dhis2ProgressIndicator(message: String? = null) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularProgressIndicator( - modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.primary, - ) - message?.let { Text(it, color = textSecondary) } - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/buttons/FAButton.kt b/ui-components/src/main/java/org/dhis2/ui/buttons/FAButton.kt deleted file mode 100644 index f2d40a1fa6..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/buttons/FAButton.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.dhis2.ui.buttons - -import androidx.annotation.StringRes -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import org.dhis2.ui.R - -@Composable -fun FAButton( - modifier: Modifier = Modifier, - @StringRes text: Int, - contentColor: Color, - containerColor: Color, - expanded: Boolean = true, - icon: @Composable - () -> Unit, - onClick: () -> Unit, -) { - ExtendedFloatingActionButton( - onClick = onClick, - modifier = modifier, - expanded = expanded, - icon = icon, - text = { Text(text = stringResource(text)) }, - contentColor = contentColor, - containerColor = containerColor, - ) -} - -@Preview -@Composable -fun ExtendedFAButtonPreview() { - FAButton( - modifier = Modifier, - text = R.string.button_extended, - contentColor = Color.DarkGray, - containerColor = Color.LightGray, - expanded = true, - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_home_positive), - contentDescription = null, - ) - }, - ) { - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/DescriptionDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/DescriptionDialog.kt deleted file mode 100644 index fbbeab2d67..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/alert/DescriptionDialog.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.dhis2.ui.dialogs.alert - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import org.dhis2.ui.R -import org.hisp.dhis.mobile.ui.designsystem.component.Button - -@Composable -fun DescriptionDialog(labelText: String, descriptionText: String, onDismiss: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(text = labelText) }, - text = { Text(text = descriptionText) }, - confirmButton = { - Button( - text = stringResource(id = R.string.action_close), - onClick = onDismiss, - ) - }, - ) -} diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt index 1b03decc2b..b126dae27d 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt @@ -9,7 +9,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -47,7 +48,7 @@ class BottomSheetDialog( var onMessageClick: () -> Unit = {}, val showDivider: Boolean = false, val content: @Composable - ((org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog) -> Unit)? = null, + ((org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog, scrollState: LazyListState) -> Unit)? = null, ) : BottomSheetDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { @@ -71,6 +72,7 @@ class BottomSheetDialog( setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { DHIS2Theme { + val scrollState = rememberLazyListState() BottomSheetShell( title = bottomSheetDialogUiModel.title, description = when (bottomSheetDialogUiModel.clickableWord) { @@ -79,16 +81,24 @@ class BottomSheetDialog( }, headerTextAlignment = bottomSheetDialogUiModel.headerTextAlignment, icon = { - Icon( - modifier = Modifier.size(Spacing24), - painter = painterResource(bottomSheetDialogUiModel.iconResource), - contentDescription = "Icon", - tint = SurfaceColor.Primary, - ) + if (bottomSheetDialogUiModel.iconResource != -1) { + Icon( + modifier = Modifier.size(Spacing24), + painter = painterResource(bottomSheetDialogUiModel.iconResource), + contentDescription = "Icon", + tint = SurfaceColor.Primary, + ) + } }, showSectionDivider = showDivider, buttonBlock = { ButtonBlock( + modifier = Modifier.padding( + top = Spacing24, + bottom = Spacing24, + start = Spacing24, + end = Spacing24, + ), primaryButton = { bottomSheetDialogUiModel.secondaryButton?.let { style -> @@ -131,14 +141,14 @@ class BottomSheetDialog( }, content = { if (content != null) { - content.invoke(this@BottomSheetDialog) + content.invoke(this@BottomSheetDialog, scrollState) } else { bottomSheetDialogUiModel.clickableWord?.let { ClickableTextContent(bottomSheetDialogUiModel.message ?: "", it) } } }, - contentScrollState = rememberScrollState(), + contentScrollState = scrollState, ) } } @@ -173,7 +183,9 @@ class BottomSheetDialog( }, ) HorizontalDivider( - modifier = Modifier.fillMaxWidth().padding(top = Spacing24), + modifier = Modifier + .fillMaxWidth() + .padding(top = Spacing24), color = TextColor.OnDisabledSurface, thickness = Border.Thin, ) diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialogContent.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialogContent.kt index cd81b20c7b..c5e8720ab0 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialogContent.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialogContent.kt @@ -243,11 +243,13 @@ fun IssueItem(fieldWithIssue: FieldWithIssue, onClick: () -> Unit) { color = textPrimary, fontSize = 14.sp, ) - Text( - text = fieldWithIssue.message, - color = textSecondary, - fontSize = 14.sp, - ) + if (fieldWithIssue.message.isNotEmpty()) { + Text( + text = fieldWithIssue.message, + color = textSecondary, + fontSize = 14.sp, + ) + } } } } @@ -308,7 +310,7 @@ fun DialogPreview3() { fun DialogPreview4() { val fieldsWithIssues = listOf( FieldWithIssue("Uid", "Age", IssueType.ERROR, ERROR_MESSAGE), - FieldWithIssue("Uid", DATE_BIRTH, IssueType.ERROR, ERROR_MESSAGE), + FieldWithIssue("Uid", DATE_BIRTH, IssueType.ERROR, ""), FieldWithIssue("Uid", DATE_BIRTH, IssueType.ERROR, ERROR_MESSAGE), FieldWithIssue("Uid", DATE_BIRTH, IssueType.ERROR, ERROR_MESSAGE), FieldWithIssue("Uid", DATE_BIRTH, IssueType.ERROR, ERROR_MESSAGE), diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt index 3e00701b7e..849ea5fd61 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/DeleteBottomSheetDialog.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.DeleteForever @@ -20,6 +21,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonBlock import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor class @@ -58,6 +61,12 @@ DeleteBottomSheetDialog( }, buttonBlock = { ButtonBlock( + modifier = Modifier.padding( + top = Spacing0, + bottom = Spacing24, + start = Spacing24, + end = Spacing24, + ), primaryButton = { Button( style = ButtonStyle.OUTLINED, diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/FieldWithIssue.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/FieldWithIssue.kt index 807f43d629..30ee351719 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/FieldWithIssue.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/FieldWithIssue.kt @@ -13,4 +13,7 @@ enum class IssueType { WARNING, ERROR_ON_COMPLETE, WARNING_ON_COMPLETE, + ; + + fun shouldShowError() = this == ERROR || this == ERROR_ON_COMPLETE || this == MANDATORY } diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureCanvas.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureCanvas.kt deleted file mode 100644 index 0bc187e22c..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureCanvas.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.dhis2.ui.dialogs.signature - -import android.view.MotionEvent -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInteropFilter - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun SignatureCanvas(modifier: Modifier = Modifier, drawing: MutableState) { - val path by remember { mutableStateOf(Path()) } - Canvas( - modifier = modifier - .fillMaxSize() - .pointerInteropFilter { - when (it.action) { - MotionEvent.ACTION_DOWN -> { - drawing.value = Offset(it.x, it.y) - path.moveTo(it.x, it.y) - } - MotionEvent.ACTION_MOVE -> { - drawing.value = Offset(it.x, it.y) - path.lineTo(it.x, it.y) - } - } - true - }.graphicsLayer { clip = true }, - ) { - drawing.value?.let { - drawPath( - path = path, - color = Color.Black, - alpha = 1f, - style = Stroke(7f), - ) - } ?: path.reset() - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialog.kt deleted file mode 100644 index 354e6888fd..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialog.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.dhis2.ui.dialogs.signature - -import android.app.Dialog -import android.graphics.Bitmap -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.Window -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.FragmentManager -import org.dhis2.ui.theme.Dhis2Theme - -const val TAG = "SignatureDialog" - -class SignatureDialog( - private val title: String, - private val onSaveSignature: ((Bitmap) -> Unit)? = null, -) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - dialog.window!!.requestFeature(Window.FEATURE_NO_TITLE) - dialog.window!!.setBackgroundDrawableResource(android.R.color.transparent) - return dialog - } - - @OptIn(ExperimentalComposeUiApi::class) - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnDetachedFromWindow, - ) - setContent { - Dhis2Theme { - SignatureDialogUi( - title = title, - onSave = { - onSaveSignature?.invoke(it) - dismiss() - }, - onCancel = { dismiss() }, - ) - } - } - } - } - - fun show(manager: FragmentManager) { - super.show(manager, TAG) - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialogUi.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialogUi.kt deleted file mode 100644 index 28f85f256b..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/signature/SignatureDialogUi.kt +++ /dev/null @@ -1,163 +0,0 @@ -package org.dhis2.ui.dialogs.signature - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.view.View -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.dhis2.ui.R -import org.dhis2.ui.theme.textSecondary -import org.dhis2.ui.utils.dashedBorder -import org.hisp.dhis.mobile.ui.designsystem.component.Button -import org.hisp.dhis.mobile.ui.designsystem.component.IconButton -import kotlin.math.roundToInt - -@ExperimentalComposeUiApi -@Composable -fun SignatureDialogUi(title: String, onSave: (Bitmap) -> Unit, onCancel: () -> Unit) { - var capturingViewBounds: Rect? = null - val view = LocalView.current - - var capturing by remember { mutableStateOf(false) } - val drawing = remember { mutableStateOf(null) } - val isSigned by remember { derivedStateOf { drawing.value != null } } - - Column( - modifier = Modifier - .background(Color.White, RoundedCornerShape(16.dp)) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text( - text = title, - style = TextStyle( - fontSize = 10.sp, - color = textSecondary, - ), - ) - Box { - SignatureCanvas( - modifier = Modifier - .height(200.dp) - .dashedBorder( - strokeWidth = 1.dp, - color = textSecondary.copy(alpha = 0.3f), - cornerRadiusDp = 8.dp, - ) - .onGloballyPositioned { - capturingViewBounds = it.boundsInRoot() - }, - drawing = drawing, - ) - if (!capturing) { - Text( - modifier = Modifier - .padding(8.dp) - .align(Alignment.TopEnd) - .background( - MaterialTheme.colorScheme.primary, - RoundedCornerShape(6.dp, 6.dp, 6.dp, 0.dp), - ) - .padding(8.dp, 4.dp), - text = stringResource(R.string.draw_here), - style = TextStyle( - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onPrimary, - ), - ) - if (isSigned) { - IconButton( - modifier = Modifier.align(Alignment.BottomEnd), - onClick = { - drawing.value = null - }, - icon = { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = stringResource(R.string.clear), - ) - }, - ) - } - } - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), - ) { - Button( - text = stringResource(R.string.cancel), - onClick = onCancel, - ) - Button( - text = stringResource(R.string.save), - onClick = { - capturing = true - }, - enabled = isSigned, - ) - } - } - - LaunchedEffect(capturing) { - capturingViewBounds - ?.takeIf { capturing } - ?.captureBitmap(view) - ?.let { onSave(it) } - } -} - -fun Rect.captureBitmap(view: View): Bitmap? { - val rect = deflate(2f) - val imageBitmap = Bitmap.createBitmap( - rect.width.roundToInt(), - rect.height.roundToInt(), - Bitmap.Config.ARGB_8888, - ) - val canvas = Canvas(imageBitmap) - .apply { - translate(-rect.left, -rect.top) - } - view.draw(canvas) - return imageBitmap -} - -@OptIn(ExperimentalComposeUiApi::class) -@Preview -@Composable -fun PreviewSignatureUI() { - SignatureDialogUi(title = "Form name", onSave = {}, onCancel = {}) -} diff --git a/ui-components/src/main/java/org/dhis2/ui/extensions/Extensions.kt b/ui-components/src/main/java/org/dhis2/ui/extensions/Extensions.kt deleted file mode 100644 index 14ee1ce20f..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/extensions/Extensions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.dhis2.ui.extensions - -import java.text.DecimalFormat - -fun Float.decimalFormat(pattern: String = "*0.##"): String { - return DecimalFormat(pattern).format(this) -} diff --git a/ui-components/src/main/java/org/dhis2/ui/inputs/FileInput.kt b/ui-components/src/main/java/org/dhis2/ui/inputs/FileInput.kt deleted file mode 100644 index 2d4b0d2659..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/inputs/FileInput.kt +++ /dev/null @@ -1,278 +0,0 @@ -package org.dhis2.ui.inputs - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import androidx.core.graphics.toColorInt -import org.dhis2.ui.R -import org.dhis2.ui.model.InputData -import org.dhis2.ui.theme.defaultFontFamily -import org.hisp.dhis.mobile.ui.designsystem.component.Button -import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle -import org.hisp.dhis.mobile.ui.designsystem.component.IconButton - -@Composable -fun BoxedInput( - leadingIcon: @Composable - (modifier: Modifier) -> Unit, - trailingIcons: @Composable - RowScope.() -> Unit, - content: @Composable - (modifier: Modifier) -> Unit, -) { - Surface( - modifier = Modifier - .wrapContentSize(), - shape = RoundedCornerShape(6.dp), - color = Color.White, - shadowElevation = 4.dp, - ) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(8.dp), - ) { - val (leadingIconRef, contentRef, trailingIconsRef) = createRefs() - leadingIcon( - Modifier.constrainAs(leadingIconRef) { - start.linkTo(parent.start) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - height = Dimension.wrapContent - }, - ) - content( - Modifier.constrainAs(contentRef) { - start.linkTo(leadingIconRef.end, 8.dp) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - end.linkTo(trailingIconsRef.start, 8.dp) - height = Dimension.fillToConstraints - width = Dimension.fillToConstraints - }, - ) - Row( - modifier = Modifier.constrainAs(trailingIconsRef) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - height = Dimension.wrapContent - }, - ) { - trailingIcons() - } - } - } -} - -@Composable -fun FileDescription(modifier: Modifier, fileInputData: InputData.FileInputData) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = fileInputData.fileName, - style = TextStyle( - color = Color.Black.copy(alpha = 0.87f), - fontSize = 12.sp, - fontFamily = defaultFontFamily, - fontWeight = FontWeight(400), - lineHeight = 20.sp, - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = fileInputData.fileSizeLabel, - style = TextStyle( - color = Color.Black.copy(alpha = 0.38f), - fontSize = 10.sp, - fontWeight = FontWeight(400), - fontFamily = defaultFontFamily, - lineHeight = 12.sp, - ), - ) - } -} - -@Composable -fun FileInput( - fileInputData: InputData.FileInputData?, - addFileLabel: String, - enabled: Boolean = true, - onAddFile: () -> Unit = {}, - onDownloadClick: () -> Unit = {}, - onDeleteFile: () -> Unit = {}, -) { - if (fileInputData != null) { - FileInputWithValue( - fileInputData = fileInputData, - enabled = enabled, - onDownloadClick = onDownloadClick, - onDeleteFile = onDeleteFile, - - ) - } else { - FileInputWithoutValue( - modifier = Modifier.fillMaxWidth(), - label = addFileLabel, - enabled = enabled, - onAddFile = onAddFile, - ) - } -} - -@Composable -fun FileInputWithoutValue( - modifier: Modifier, - label: String, - enabled: Boolean, - onAddFile: () -> Unit, -) { - Button( - modifier = modifier, - enabled = enabled, - style = ButtonStyle.OUTLINED, - onClick = onAddFile, - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_file), - contentDescription = "", - ) - }, - text = label, - ) -} - -@Composable -fun FileInputWithValue( - fileInputData: InputData.FileInputData, - enabled: Boolean, - onDownloadClick: () -> Unit, - onDeleteFile: () -> Unit, -) { - BoxedInput( - leadingIcon = { modifier -> - Box( - modifier = modifier - .size(LocalViewConfiguration.current.minimumTouchTargetSize), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_file), - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - } - }, - trailingIcons = { - IconButton( - enabled = enabled, - onClick = onDownloadClick, - icon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_file_download), - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - }, - ) - - IconButton( - enabled = enabled, - onClick = { onDeleteFile() }, - icon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - }, - ) - }, - ) { modifier -> - FileDescription(modifier = modifier, fileInputData = fileInputData) - } -} - -@Composable -@Preview -fun FileWithoutValueInputTest() { - FileInput(fileInputData = null, addFileLabel = "addFile") -} - -@Composable -@Preview -fun FileWithValueInputTest() { - FileInput(fileInputData = null, addFileLabel = "addFile") -} - -@Composable -@Preview -fun FileInputWithMessageTest() { - FormInputBox( - labelText = "This is the label", - helperText = "This is a messsage", - descriptionText = "This is a description", - selected = true, - labelTextColor = Color.Black.copy(alpha = 0.54f), - helperTextColor = Color("#E91E63".toColorInt()), - ) { - FileInput( - fileInputData = InputData.FileInputData( - fileName = "file.txt", - fileSize = 1234, - filePath = "/file.txt", - ), - addFileLabel = "addFile", - ) - } -} - -@Composable -@Preview -fun FileInputNoValueWithMessageTest() { - FormInputBox( - labelText = "This is the label", - helperText = "This is a messsage", - descriptionText = "This is a description", - selected = true, - labelTextColor = Color.Black.copy(alpha = 0.54f), - helperTextColor = Color("#E91E63".toColorInt()), - ) { - FileInput( - fileInputData = null, - addFileLabel = "addFile", - ) - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/inputs/FormInputBox.kt b/ui-components/src/main/java/org/dhis2/ui/inputs/FormInputBox.kt deleted file mode 100644 index ff3f4ebb9d..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/inputs/FormInputBox.kt +++ /dev/null @@ -1,151 +0,0 @@ -package org.dhis2.ui.inputs - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.RoundRect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.dhis2.ui.R -import org.dhis2.ui.dialogs.alert.DescriptionDialog - -@Composable -fun FormInputBox( - labelText: String?, - helperText: String? = null, - descriptionText: String? = null, - selected: Boolean = false, - enabled: Boolean = true, - labelTextColor: Color, - helperTextColor: Color = Color.Black.copy(alpha = 0.38f), - content: @Composable - () -> Unit, -) { - val openDescriptionDialog = remember { mutableStateOf(false) } - Box( - modifier = Modifier - .wrapContentHeight() - .alpha(1.0f.takeIf { enabled } ?: 0.5f), - ) { - Column( - modifier = Modifier - .wrapContentHeight() - .padding( - top = 9.dp, - bottom = 16.dp, - ) - .drawInputSelector( - selected = selected, - color = MaterialTheme.colorScheme.primary, - ) - .padding( - start = 16.dp, - end = 16.dp, - ) - .graphicsLayer { clip = false }, - verticalArrangement = spacedBy(9.dp), - ) { - labelText?.let { - Row( - modifier = Modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(8.dp), - ) { - HelperText( - helperText = labelText, - textStyle = TextStyle( - color = labelTextColor, - fontSize = 10.sp, - lineHeight = 10.sp, - ), - ) - descriptionText?.let { - Icon( - modifier = Modifier - .size(10.dp) - .clickable { openDescriptionDialog.value = true }, - imageVector = ImageVector.vectorResource(id = R.drawable.ic_input_info), - contentDescription = "", - tint = MaterialTheme.colorScheme.primary, - ) - } - } - } - content() - helperText?.let { - HelperText( - helperText = helperText, - textStyle = TextStyle( - color = helperTextColor, - fontSize = 10.sp, - lineHeight = 12.sp, - ), - ) - } - } - - if (openDescriptionDialog.value) { - DescriptionDialog(labelText!!, descriptionText!!) { - openDescriptionDialog.value = false - } - } - } -} - -@Composable -fun HelperText(helperText: String, textStyle: TextStyle) { - Text( - text = helperText, - style = textStyle, - ) -} - -fun Modifier.drawInputSelector(selected: Boolean, color: Color) = when (selected) { - true -> this.then( - drawBehind { - drawPath( - Path().apply { - addRoundRect( - RoundRect( - rect = Rect( - offset = Offset(8.dp.toPx(), 0f), - size = Size(2.dp.toPx(), size.height), - ), - topLeft = CornerRadius(10f, 10f), - topRight = CornerRadius(10f, 10f), - bottomLeft = CornerRadius(10f, 10f), - bottomRight = CornerRadius(10f, 10f), - ), - ) - }, - color = color, - ) - }, - ) - else -> this -} diff --git a/ui-components/src/main/java/org/dhis2/ui/inputs/PictureInput.kt b/ui-components/src/main/java/org/dhis2/ui/inputs/PictureInput.kt deleted file mode 100644 index 8ba56d8ffc..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/inputs/PictureInput.kt +++ /dev/null @@ -1,122 +0,0 @@ -package org.dhis2.ui.inputs - -import android.graphics.Bitmap -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.dhis2.ui.theme.errorColor -import org.hisp.dhis.mobile.ui.designsystem.component.Button -import org.hisp.dhis.mobile.ui.designsystem.component.IconButton -import org.hisp.dhis.mobile.ui.designsystem.resource.provideDHIS2Icon - -@Composable -fun PictureInput( - imageValue: Bitmap?, - enabled: Boolean = true, - addButtonData: AddButtonData, - onClick: () -> Unit, - onClear: () -> Unit, -) { - if (imageValue != null) { - Picture(imageValue.asImageBitmap(), enabled, onClick, onClear) - } else { - Button( - modifier = Modifier.fillMaxWidth(), - enabled = enabled, - text = addButtonData.label, - icon = { - Icon( - painter = addButtonData.icon, - contentDescription = "", - ) - }, - onClick = addButtonData.onClick, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Picture(image: ImageBitmap, enabled: Boolean, onClick: () -> Unit, onClear: () -> Unit) { - Box { - Surface( - onClick = onClick, - shadowElevation = 4.dp, - shape = RoundedCornerShape(6.dp), - ) { - Image( - modifier = Modifier.defaultMinSize( - minWidth = if (image.width >= image.height) 200.dp else 0.dp, - minHeight = if (image.width < image.height) 200.dp else 0.dp, - ), - bitmap = image, - contentScale = ContentScale.Crop, - contentDescription = "picture", - ) - } - if (enabled) { - IconButton( - modifier = Modifier - .padding(8.dp) - .background(Color.White, CircleShape) - .size(40.dp) - .align(Alignment.TopEnd), - onClick = onClear, - icon = { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "clear", - tint = errorColor, - ) - }, - ) - } - } -} - -@Preview -@Composable -fun IconClear() { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "clear", - tint = errorColor, - ) -} - -@Preview -@Composable -fun IconClearUI() { - Icon( - painter = provideDHIS2Icon(resourceName = "dhis2_microscope_outline"), - contentDescription = "clear", - tint = errorColor, - ) -} - -data class AddButtonData( - val icon: Painter, - val label: String, - val onClick: () -> Unit, -) diff --git a/ui-components/src/main/java/org/dhis2/ui/model/InputData.kt b/ui-components/src/main/java/org/dhis2/ui/model/InputData.kt deleted file mode 100644 index 806ea176af..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/model/InputData.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.dhis2.ui.model - -import org.dhis2.ui.extensions.decimalFormat - -sealed class InputData { - data class FileInputData( - val fileName: String, - private val fileSize: Long, - val filePath: String, - ) { - val fileSizeLabel - get() = run { - val kb = fileSize / 1024f - val mb = kb / 1024f - if (kb < 1024f) { - "${kb.decimalFormat("*0")}KB" - } else { - "${mb.decimalFormat()}MB" - } - } - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/sync/SyncButtonProvider.kt b/ui-components/src/main/java/org/dhis2/ui/sync/SyncButtonProvider.kt deleted file mode 100644 index bec020aa45..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/sync/SyncButtonProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.dhis2.ui.sync - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Sync -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import org.hisp.dhis.mobile.ui.designsystem.component.Button -import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle -import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor - -@Composable -private fun ProvideSyncButton(text: String?, onSyncIconClick: () -> Unit) { - text?.let { - Button( - style = ButtonStyle.TONAL, - text = it, - icon = { - Icon( - imageVector = Icons.Outlined.Sync, - contentDescription = it, - tint = TextColor.OnPrimaryContainer, - ) - }, - onClick = onSyncIconClick, - modifier = Modifier.fillMaxWidth(), - ) - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/theme/Color.kt b/ui-components/src/main/java/org/dhis2/ui/theme/Color.kt index 319a999a50..8b58a1c740 100644 --- a/ui-components/src/main/java/org/dhis2/ui/theme/Color.kt +++ b/ui-components/src/main/java/org/dhis2/ui/theme/Color.kt @@ -2,9 +2,6 @@ package org.dhis2.ui.theme import androidx.compose.ui.graphics.Color -val contrastDark = Color(0xB3000000) -val contrastLight = Color(0xE6FFFFFF) - val programColorDark = Color(0xFF00BCD4) val programColorLight = Color(0xFF84FFFF) @@ -12,67 +9,4 @@ val textPrimary = Color(0xDE000000) val textSecondary = Color(0x8A000000) val textSubtitle = Color(0x61000000) val warningColor = Color(0xFFFF9800) -val errorColor = Color(0xFFE91E63) val colorPrimary = Color(0xFF2C98F0) - -val md_theme_light_primary = Color(0xFF2C98F0) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFFD1E4FF) -val md_theme_light_onPrimaryContainer = Color(0xFF001D35) -val md_theme_light_secondary = Color(0xFF4CAF50) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFF94F990) -val md_theme_light_onSecondaryContainer = Color(0xFF002204) -val md_theme_light_tertiary = Color(0xFFFF9800) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFFFDCBE) -val md_theme_light_onTertiaryContainer = Color(0xFF2C1600) -val md_theme_light_error = Color(0xFF2C98F0) -val md_theme_light_errorContainer = Color(0xFFFFFFFF) -val md_theme_light_onError = Color(0xFFFFD9DE) -val md_theme_light_onErrorContainer = Color(0xFF400014) -val md_theme_light_background = Color(0xFFFDFCFF) -val md_theme_light_onBackground = Color(0xFF1A1C1E) -val md_theme_light_surface = Color(0xFFFDFCFF) -val md_theme_light_onSurface = Color(0xFF1A1C1E) -val md_theme_light_surfaceVariant = Color(0xFFDFE2EB) -val md_theme_light_onSurfaceVariant = Color(0xFF42474E) -val md_theme_light_outline = Color(0xFF73777F) -val md_theme_light_inverseOnSurface = Color(0xFFF1F0F4) -val md_theme_light_inverseSurface = Color(0xFF2F3033) -val md_theme_light_inversePrimary = Color(0xFF9DCAFF) -val md_theme_light_shadow = Color(0xFF000000) -val md_theme_light_surfaceTint = Color(0xFF0062A2) -val md_theme_light_outlineVariant = Color(0xFFC3C7CF) -val md_theme_light_scrim = Color(0xFF000000) - -val md_theme_dark_primary = Color(0xFF9DCAFF) -val md_theme_dark_onPrimary = Color(0xFF003257) -val md_theme_dark_primaryContainer = Color(0xFF00497C) -val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF) -val md_theme_dark_secondary = Color(0xFF78DC77) -val md_theme_dark_onSecondary = Color(0xFF00390A) -val md_theme_dark_secondaryContainer = Color(0xFF005313) -val md_theme_dark_onSecondaryContainer = Color(0xFF94F990) -val md_theme_dark_tertiary = Color(0xFFFFB870) -val md_theme_dark_onTertiary = Color(0xFF4A2800) -val md_theme_dark_tertiaryContainer = Color(0xFF693C00) -val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCBE) -val md_theme_dark_error = Color(0xFFFFB2BE) -val md_theme_dark_errorContainer = Color(0xFF660025) -val md_theme_dark_onError = Color(0xFF900038) -val md_theme_dark_onErrorContainer = Color(0xFFFFD9DE) -val md_theme_dark_background = Color(0xFF1A1C1E) -val md_theme_dark_onBackground = Color(0xFFE2E2E6) -val md_theme_dark_surface = Color(0xFF1A1C1E) -val md_theme_dark_onSurface = Color(0xFFE2E2E6) -val md_theme_dark_surfaceVariant = Color(0xFF42474E) -val md_theme_dark_onSurfaceVariant = Color(0xFFC3C7CF) -val md_theme_dark_outline = Color(0xFF8D9199) -val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E) -val md_theme_dark_inverseSurface = Color(0xFFE2E2E6) -val md_theme_dark_inversePrimary = Color(0xFF0062A2) -val md_theme_dark_shadow = Color(0xFF000000) -val md_theme_dark_surfaceTint = Color(0xFF9DCAFF) -val md_theme_dark_outlineVariant = Color(0xFF42474E) -val md_theme_dark_scrim = Color(0xFF000000) diff --git a/ui-components/src/main/java/org/dhis2/ui/theme/Theme.kt b/ui-components/src/main/java/org/dhis2/ui/theme/Theme.kt index d1173d3ad1..9026e316b0 100644 --- a/ui-components/src/main/java/org/dhis2/ui/theme/Theme.kt +++ b/ui-components/src/main/java/org/dhis2/ui/theme/Theme.kt @@ -1,74 +1,8 @@ package org.dhis2.ui.theme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import com.google.accompanist.themeadapter.material3.Mdc3Theme -private val LightColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, -) - -private val DarkColors = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, -) - @Composable fun Dhis2Theme(content: @Composable () -> Unit) { Mdc3Theme(content = content) diff --git a/ui-components/src/main/java/org/dhis2/ui/theme/Type.kt b/ui-components/src/main/java/org/dhis2/ui/theme/Type.kt deleted file mode 100644 index 6501bfce54..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/theme/Type.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.dhis2.ui.theme - -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import org.dhis2.ui.R - -val defaultFontFamily = FontFamily( - Font(R.font.rubik_regular), - Font(R.font.rubik_bold, FontWeight.Bold), - Font(R.font.rubik_light, FontWeight.Light), -) - -val descriptionTextStyle = TextStyle( - color = Color(0xFF667685), - fontSize = 10.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily(Font(R.font.roboto_regular)), - lineHeight = 16.sp, - letterSpacing = (0.4).sp, - textAlign = TextAlign.End, -) diff --git a/ui-components/src/main/java/org/dhis2/ui/utils/ColorUtils.kt b/ui-components/src/main/java/org/dhis2/ui/utils/ColorUtils.kt deleted file mode 100644 index c605eb09a3..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/utils/ColorUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.dhis2.ui.utils - -import androidx.compose.ui.graphics.Color -import androidx.core.graphics.blue -import androidx.core.graphics.green -import androidx.core.graphics.red -import org.dhis2.ui.theme.contrastDark -import org.dhis2.ui.theme.contrastLight -import kotlin.math.pow - -fun Int.getAlphaContrastColor(): Color { - val rgb = listOf( - red / 255.0, - green / 255.0, - blue / 255.0, - ).map { - when { - it <= 0.03928 -> it / 12.92 - else -> ((it + 0.055) / 1.055).pow(2.4) - } - } - val l = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] - return when { - l > 0.500 -> contrastDark - else -> contrastLight - } -} diff --git a/ui-components/src/main/java/org/dhis2/ui/utils/Modifiers.kt b/ui-components/src/main/java/org/dhis2/ui/utils/Modifiers.kt deleted file mode 100644 index 4893285ef7..0000000000 --- a/ui-components/src/main/java/org/dhis2/ui/utils/Modifiers.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.dhis2.ui.utils - -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp - -fun Modifier.dashedBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed( - factory = { - val density = LocalDensity.current - val strokeWidthPx = density.run { strokeWidth.toPx() } - val cornerRadiusPx = density.run { cornerRadiusDp.toPx() } - - this.then( - Modifier.drawWithCache { - onDrawBehind { - val stroke = Stroke( - width = strokeWidthPx, - pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f), - ) - - drawRoundRect( - color = color, - style = stroke, - cornerRadius = CornerRadius(cornerRadiusPx), - ) - } - }, - ) - }, -) diff --git a/ui-components/src/main/res/values-es/strings.xml b/ui-components/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..511e942b0d --- /dev/null +++ b/ui-components/src/main/res/values-es/strings.xml @@ -0,0 +1,27 @@ + + + Completar + Descartar los cambios + Ahora no + Seguir editando + Revisar + Cancelar + Guardar + Limpiar + Dibujar aquí + Hecho + Cancelar + Cerrar + Buscar + Limpiar todo + + %s error + %s errores + %s errores + + + %s advertencia + %s advertencias + %s advertencias + + \ No newline at end of file diff --git a/ui-components/src/main/res/values-fr/strings.xml b/ui-components/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..75aa393aac --- /dev/null +++ b/ui-components/src/main/res/values-fr/strings.xml @@ -0,0 +1,18 @@ + + + Pas maintenant + Annuler + Effacer + Fermer + Rechercher + + %s erreur + %s erreurs + %s erreurs + + + %s avertissement + %s avertissements + %s avertissements + + \ No newline at end of file diff --git a/ui-components/src/main/res/values-pt/strings.xml b/ui-components/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..2b34c177df --- /dev/null +++ b/ui-components/src/main/res/values-pt/strings.xml @@ -0,0 +1,27 @@ + + + Concluir + Descartar alterações + Agora não + Continue a editar + Revisão + Cancelar + Guardar + Limpar + Desenhar aqui + concluído + Cancelar + Fechar + Pesquisar + Limpar tudo + + %s erro + %s erros + %s Erros + + + %s aviso + %s avisos + %s Alertas + + \ No newline at end of file diff --git a/ui-components/src/main/res/values-vi/strings.xml b/ui-components/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..8af5d33ccf --- /dev/null +++ b/ui-components/src/main/res/values-vi/strings.xml @@ -0,0 +1,23 @@ + + + Hoàn tất + Hủy bỏ các thay đổi + Không phải bây giờ + Tiếp tục chỉnh sửa + Xem lại + Hủy + Lưu + Xóa + Vẽ ở đây + Hoàn thành + Hủy + Đóng + Tìm kiếm + Xóa tất cả + + Các lỗi %s + + + Các cảnh báo %s + + \ No newline at end of file diff --git a/ui-components/src/main/res/values-zh/strings.xml b/ui-components/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..5a20c6f8bb --- /dev/null +++ b/ui-components/src/main/res/values-zh/strings.xml @@ -0,0 +1,23 @@ + + + 完成 + 忽略修改 + 现在不要 + 继续编辑 + 审查 + 取消 + 保存 + 清除 + 在此画图 + 完成 + 取消 + 关闭 + 搜索 + 全清除 + + %s 错误 + + + %s 警告 + + \ No newline at end of file diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index d8699e3b3a..dcb99e68af 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,9 @@ This is a patch version that fixes: -- ANDROAPP-6653 Large option sets freeze the app -- ANDROAPP-6665 Filters persists when exiting the program or data set -- ANDROAPP-6691 NullPointerException: Dataset table - + -ANDROAPP-5888 RTSM - Stock distribution allows entry of zero values + -ANDROAPP-6108 Bottom sheet Icon button is displaced + -ANDROAPP-6715 Crash when selecting a checkbox option set + -ANDROAPP-6220 Login error - Fragmentation has been destroyed + -ANDROAPP-6281 Formatting of org unit selector buttons over android navigation bar + -ANDROAPP-6282 Misalignment of TEI list and dashboard cards + -... You can find all the details on Jira and Github. \ No newline at end of file