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/.tx/config b/.tx/config index 0fa80f633d..afd2c81566 100644 --- a/.tx/config +++ b/.tx/config @@ -1,6 +1,8 @@ [main] host = https://www.transifex.com -lang_map = fa_AF: prs, uz@Cyrl: uz, uz@Latn: b+uz+Latn, zh_CN: zh-rCN, pt_BR: pt-rBR, es_419: b+es+419 +lang_map = fa_AF: prs, uz@Cyrl: uz, uz@Latn: b+uz+Latn, zh_CN: zh-rCN, pt_BR: pt-rBR, +es_419: b+es+419, ar_EG: ar-rEG, sw_TZ: sw-rTZ, hi_IN: hi, en_US: en-rUS, ar_IQ: ar-rIQ, +ko_KR: ko-rKR, zh_HK: zh-rHK, ar_SD: ar-rSD [o:hisp-uio:p:dhis2-android-capture-app:r:stock-strings-xml] file_filter = stock-usecase/src/main/res/values-/strings.xml @@ -8,6 +10,7 @@ source_file = stock-usecase/src/main/res/values/strings.xml source_lang = en type = ANDROID minimum_perc = 0 +resource_name = LMIS [o:hisp-uio:p:dhis2-android-capture-app:r:analytics-strings-xml] file_filter = dhis_android_analytics/src/main/res/values-/strings.xml diff --git a/Jenkinsfile b/Jenkinsfile index 60ad35fff2..6eb3ac12de 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/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/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/data/server/ServerModule.kt b/app/src/main/java/org/dhis2/data/server/ServerModule.kt index 75d2462eb2..c9be356f8e 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerModule.kt +++ b/app/src/main/java/org/dhis2/data/server/ServerModule.kt @@ -177,9 +177,6 @@ class ServerModule { 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/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/events/EventInfoProvider.kt b/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt index a2da03a56f..518741c5ec 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, ) } 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..9088fd219e 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 @@ -35,15 +35,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 +78,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 +92,29 @@ fun ProvideInputDate( ), yearRange = yearRange, ), + inputTextFieldValue = value, + inputState = state, + ) + InputDateTime( + state = inputState, modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), + onValueChanged = { + value = it ?: TextFieldValue() + state = getInputShellStateBasedOnValue(it?.text) + it?.let { it1 -> manageActionBasedOnValue(uiModel, it1.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 } @@ -132,7 +125,7 @@ fun isValidDateFormat(dateString: String): Boolean { fun getInputShellStateBasedOnValue(dateString: String?): InputShellState { dateString?.let { - return if (isValid(it) && !isValidDateFormat(it)) { + return if (!isValidDateFormat(it)) { InputShellState.ERROR } else { InputShellState.FOCUSED @@ -144,7 +137,7 @@ fun getInputShellStateBasedOnValue(dateString: String?): InputShellState { 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) } @@ -175,14 +168,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 = kotlinx.datetime.LocalDate.Formats.ISO.parse(dateValue) + InputDateValues(date.dayOfMonth, date.monthNumber, date.year) } } 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..15c9d74c49 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt @@ -27,6 +27,7 @@ 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 @@ -93,13 +94,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 +133,14 @@ class MainActivity : ) mainComponent.inject(this@MainActivity) } ?: navigateTo(true) + mainNavigator = MainNavigator( + dispatcherProvider = presenter.dispatcherProvider, + supportFragmentManager, + { /*no-op*/ }, + ) { titleRes, _, showBottomNavigation -> + setTitle(getString(titleRes)) + setBottomNavigationVisibility(showBottomNavigation) + } super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 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/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/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/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/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 7aecc48978..bebd188c08 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; @@ -121,6 +122,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 +136,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 +158,7 @@ public class SearchRepositoryImpl implements SearchRepository { currentProgram, resources); this.metadataIconProvider = metadataIconProvider; + this.profilePictureProvider = profilePictureProvider; } @@ -389,39 +393,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()); } } @@ -768,7 +750,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/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/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/dashboardfragments/relationships/RelationshipModule.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java index 119258cb81..8c243f102e 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,6 +4,8 @@ import org.dhis2.commons.data.ProgramConfigurationRepository; import org.dhis2.commons.date.DateLabelProvider; +import org.dhis2.commons.date.DateUtils; +import org.dhis2.commons.di.dagger.PerActivity; import org.dhis2.commons.di.dagger.PerFragment; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; @@ -96,7 +98,8 @@ RelationshipMapsRepository providesRepository( D2 d2, ResourceManager resourceManager, MetadataIconProvider metadataIconProvider, - DateLabelProvider dateLabelProvider + DateLabelProvider dateLabelProvider, + DateUtils dateUtils ) { RelationshipConfiguration config; if (teiUid != null) { @@ -119,7 +122,8 @@ RelationshipMapsRepository providesRepository( resourceManager, dateLabelProvider, metadataIconProvider, - profilePictureProvider + profilePictureProvider, + dateUtils ) ); } @@ -155,6 +159,13 @@ RelationshipsViewModel provideRelationshipsViewModel( ); } + @Provides + @PerFragment + DateUtils provideDateUtils( + ) { + return DateUtils.getInstance(); + } + @Provides @PerFragment GetRelationshipsByType provideGetRelationshipsByType( 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/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/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index 9c2070b1d4..28a9d8017b 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 @@ -215,10 +215,9 @@ class SchedulingViewModel( d2.eventModule().events().uid(eventUid).run { setDueDate(dueDate.currentDate) setStatus(EventStatus.SCHEDULE) + onDueDateUpdated?.invoke() } } - - onDueDateUpdated?.invoke() } } 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/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/res/layout/activity_program_event_detail.xml b/app/src/main/res/layout/activity_program_event_detail.xml index 385a676100..72e09e38d9 100644 --- a/app/src/main/res/layout/activity_program_event_detail.xml +++ b/app/src/main/res/layout/activity_program_event_detail.xml @@ -38,9 +38,9 @@ style="@style/ActionIcon" android:layout_marginStart="4dp" android:onClick="@{()->presenter.onBackClick()}" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_arrow_back" tools:ignore="ContentDescription" /> @@ -63,8 +63,8 @@ + app:layout_constraintTop_toBottomOf="@+id/toolbar_guideline" /> - - - - + app:layout_constraintTop_toBottomOf="@id/backdropGuideTop" /> 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/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/commons/build.gradle.kts b/commons/build.gradle.kts index cb40e1fb9e..546f96cceb 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 } 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/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index 490f46de22..d2d0010c6b 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 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/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/res/values/strings.xml b/commons/src/main/res/values/strings.xml index b00ca0e018..04c63f2ac3 100644 --- a/commons/src/main/res/values/strings.xml +++ b/commons/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ 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... 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..efe1659177 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 @@ -16,8 +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 androidx.compose.ui.unit.sp import com.google.android.material.composethemeadapter.MdcTheme import dhis2.org.R import dhis2.org.analytics.charts.data.ChartType @@ -41,8 +47,8 @@ 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.TextColor import kotlin.math.roundToInt private const val LINE_LISTING_MAX_ROWS = 500 @@ -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/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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d65f0672a..089a5d6e6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,13 +2,13 @@ sdk = "34" minSdk = "21" vCode = "138" -vName = "3.1.0.1" -gradle = "8.6.1" +vName = "3.1.1" +gradle = "8.7.2" kotlin = '2.0.20' hilt = '2.47' jacoco = '0.8.10' -designSystem = "0.4.0.1" -dhis2sdk = "1.11.0.1" +designSystem = "0.4.1-SNAPSHOT" +dhis2sdk = "1.11.1-SNAPSHOT" ruleEngine = "3.0.0" expressionParser = "1.1.0" appcompat = "1.6.1" @@ -49,13 +49,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" @@ -168,13 +161,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" } @@ -238,8 +224,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/ui/home/screens/HomeScreen.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/home/screens/HomeScreen.kt index 8761707717..a551e0b902 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,18 +110,18 @@ fun HomeScreen( label = "HomeScreenContent", ) { targetIndex -> when (targetIndex) { - BottomNavigation.ANALYTICS.id -> - { - DHIS2Theme() {} - AnalyticsScreen( - viewModel = viewModel, - backAction = { manageStockViewModel.onHandleBackNavigation() }, - themeColor = themeColor, - modifier = Modifier.padding(paddingValues), - scaffoldState = scaffoldState, - supportFragmentManager = supportFragmentManager, - ) - } + BottomNavigation.ANALYTICS.id -> { + DHIS2Theme() {} + AnalyticsScreen( + viewModel = viewModel, + backAction = { manageStockViewModel.onHandleBackNavigation() }, + themeColor = themeColor, + modifier = Modifier.padding(paddingValues), + scaffoldState = scaffoldState, + supportFragmentManager = supportFragmentManager, + ) + } + BottomNavigation.DATA_ENTRY.id -> { Backdrop( activity = activity, 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/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/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), - ) - } - }, - ) - }, -)