diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt index 61a935da81..c1e199892b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt @@ -51,42 +51,6 @@ class SyncFlowTest : BaseTest() { ApplicationProvider.getApplicationContext().mutableWorkInfoStatuses } - @Test - fun shouldSuccessfullySyncAChangedTEI() { - val teiName = "Scott" - val teiLastName = "Kelley" - - prepareTBProgrammeIntentAndLaunchActivity(ruleSearch) - searchTeiRobot { - clickOnOpenSearch() - typeAttributeAtPosition(teiName, 0) - typeAttributeAtPosition(teiLastName, 1) - clickOnSearch() - clickOnTEI(teiName, teiLastName) - } - - teiDashboardRobot { - clickOnGroupEventByName(TB_VISIT) - clickOnEventWith(TB_VISIT_EVENT_DATE, ORG_UNIT) - } - - eventRobot { - clickOnUpdate() - } - - teiDashboardRobot { - composeTestRule.onNodeWithText("Sync").performClick() - } - syncFlowRobot { - waitToDebounce(500) - clickOnSyncButton(composeTestRule) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) - workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - checkSyncWasSuccessfully(composeTestRule) - } - cleanLocalDatabase() - } - @Test fun shouldShowErrorWhenTEISyncFails() { val teiName = "Lars" diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt new file mode 100644 index 0000000000..29af01483d --- /dev/null +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt @@ -0,0 +1,184 @@ +package org.dhis2.usescases.teidashboard + +import android.view.View +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.activityScenarioRule +import dhis2.org.analytics.charts.Charts +import io.reactivex.Observable +import org.dhis2.R +import org.dhis2.android.rtsm.utils.NetworkUtils +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.ui.ThemeManager +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.EventInitialTest +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository +import org.dhis2.usescases.main.program.ProgramPresenter +import org.dhis2.usescases.teiDashboard.DashboardRepositoryImpl +import org.dhis2.usescases.teiDashboard.DashboardViewModel +import org.dhis2.usescases.teiDashboard.TeiAttributesProvider +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts +import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity +import org.dhis2.utils.analytics.AnalyticsHelper +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventEditableStatus +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.TrackedEntityType +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doReturnConsecutively +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Calendar + +class TeiDashboardMobileActivityTest { + + @get:Rule + val activityScenarioRule = activityScenarioRule() + + @get:Rule + val composeRule = createAndroidComposeRule() + + + private lateinit var viewModel: DashboardViewModel + + private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) + private val resources: ResourceManager = mock() + private val charts: Charts = mock() + private val teiAttributesProvider: TeiAttributesProvider = mock() + + + private var repository: DashboardRepositoryImpl = mock { + + } + var tei = Observable.just(TrackedEntityInstance.builder() + .uid(TEI_Uid) + .created(Calendar.getInstance().time) + .lastUpdated(Calendar.getInstance().time) + .organisationUnit(ORG_UNIT_UID) + .trackedEntityType(TETYPE_NAME) + .build()) + + private val teType: TrackedEntityType = mock() + + private val analyticsHelper = mock { + } + + private val themeManager: ThemeManager = mock() + private val presenter: TeiDashboardContracts.Presenter = mock() + private val filterManager: FilterManager = mock() + private val networkUtils: NetworkUtils = mock() + + companion object { + const val ENROLLMENT_UID = "enrollmentUid" + const val TEI_Uid = "TEIUid" + const val PROGRAM_UID = "programUid" + const val TETYPE_NAME = "TETypeName" + const val INITIAL_ORG_UNIT_UID = "initialOrgUnitUid" + const val PROGRAM_STAGE_NAME = "Marvellous Program Stage" + const val EXECUTION_DATE = "Date of Marvellous Program Stage" + const val ORG_UNIT_UID = "orgUnitUid" + const val ENROLLMENT_VALUE_WITH_NOTE = "EnrollmentValueWithNote" + const val TEI_UID_VALUE_WITH_NOTE = "TeiUidValueWithNote" + const val CHILD_PROGRAM_UID_VALUE = "childProgramUid" + } + + private fun initViewModel() { + viewModel = DashboardViewModel( + repository , + analyticsHelper + ) + + } + + private fun setUp() { + initRepository() + initViewModel() + } + + private fun initRepository() { + repository = DashboardRepositoryImpl( + d2, + charts, + TEI_Uid, + PROGRAM_UID, + ENROLLMENT_UID, + resources, + teiAttributesProvider, + ) + + + } + + + @Test + fun shouldSuccessfullyInitializeTeiDashBoardMobileActivity() { + setUp() + whenever (repository.getTETypeName()) doReturn TETYPE_NAME + whenever ( repository.getTrackedEntityInstance("") ) doReturn mock() + whenever {repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + } } doReturn mock() + whenever {repository.getTrackedEntityInstance("").flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + }.blockingFirst() } doReturn { teType } + whenever( + presenter.teType + ) doReturn TETYPE_NAME + + whenever( + repository.getTETypeName() + ) doReturn TETYPE_NAME + whenever( + d2.trackedEntityModule() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("") ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityInstances().byUid().eq("").one() + .blockingGet() ) doReturn mock() + + whenever( + d2.trackedEntityModule().trackedEntityTypes() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("") ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() ) doReturn mock() + whenever( + d2.trackedEntityModule().trackedEntityTypes().uid("").get().toObservable() ) doReturn mock() + + + + + ActivityScenario.launch(TeiDashboardMobileActivity::class.java).onActivity { activity -> + + val showMoreOptions = activity.findViewById(R.id.moreOptions) + showMoreOptions.performClick() + } + + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt index 7e6ab907dd..d080db8ae1 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -168,17 +168,6 @@ class TeiDashboardTest : BaseTest() { } } - @Test - fun shouldNotBeAbleToCreateNewEventsWhenFull() { - prepareTeiOpenedWithFullEventsAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - checkCanNotAddEvent() - } - } - @Test fun shouldOpenEventAndSaveSuccessfully() { setupCredentials() @@ -235,32 +224,6 @@ class TeiDashboardTest : BaseTest() { } } - @Test - fun shouldSuccessfullyCreateANewEvent() { - prepareTeiToCreateANewEventAndLaunchActivity(rule) - - teiDashboardRobot { - clickOnMenuMoreOptions() - clickOnTimelineEvents() - clickOnFab() - clickOnCreateNewEvent() - clickOnFirstReferralEvent() - waitToDebounce(2000) - clickOnReferralNextButton() - waitToDebounce(600) - } - - eventRobot { - fillRadioButtonForm(4) - clickOnFormFabButton() - clickOnNotNow(composeTestRule) - } - - teiDashboardRobot { - checkEventWasCreatedAndOpen(LAB_MONITORING, 0) - } - } - @Test fun shouldOpenEventEditAndSaveSuccessfully() { prepareTeiOpenedToEditAndLaunchActivity(rule) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java deleted file mode 100644 index 29b2c91e30..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.java +++ /dev/null @@ -1,499 +0,0 @@ -package org.dhis2.usescases.teiDashboard; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.PairKt; - -import org.dhis2.R; -import org.dhis2.commons.data.tuples.Pair; -import org.dhis2.commons.data.tuples.Trio; -import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.utils.AuthorityException; -import org.dhis2.utils.DateUtils; -import org.dhis2.utils.ValueUtils; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.android.core.arch.helpers.UidsHelper; -import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope; -import org.hisp.dhis.android.core.category.CategoryCombo; -import org.hisp.dhis.android.core.category.CategoryOptionCombo; -import org.hisp.dhis.android.core.common.ObjectWithUid; -import org.hisp.dhis.android.core.common.State; -import org.hisp.dhis.android.core.common.ValueType; -import org.hisp.dhis.android.core.enrollment.Enrollment; -import org.hisp.dhis.android.core.enrollment.EnrollmentCollectionRepository; -import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository; -import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; -import org.hisp.dhis.android.core.event.Event; -import org.hisp.dhis.android.core.event.EventStatus; -import org.hisp.dhis.android.core.legendset.Legend; -import org.hisp.dhis.android.core.maintenance.D2Error; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; -import org.hisp.dhis.android.core.program.Program; -import org.hisp.dhis.android.core.program.ProgramIndicator; -import org.hisp.dhis.android.core.program.ProgramRuleActionType; -import org.hisp.dhis.android.core.program.ProgramStage; -import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute; -import org.hisp.dhis.android.core.relationship.RelationshipType; -import org.hisp.dhis.android.core.systeminfo.SystemInfo; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityType; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityTypeAttribute; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; - -import dhis2.org.analytics.charts.Charts; -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Single; -import timber.log.Timber; - -public class DashboardRepositoryImpl implements DashboardRepository { - - private final D2 d2; - private final ResourceManager resources; - private final String enrollmentUid; - @Nullable - private final Charts charts; - - private String teiUid; - - private String programUid; - - private TeiAttributesProvider teiAttributesProvider; - - - public DashboardRepositoryImpl(D2 d2, - @Nullable Charts charts, - String teiUid, - String programUid, - String enrollmentUid, - ResourceManager resources, - TeiAttributesProvider teiAttributesProvider) { - this.d2 = d2; - this.teiUid = teiUid; - this.programUid = programUid; - this.enrollmentUid = enrollmentUid; - this.resources = resources; - this.charts = charts; - this.teiAttributesProvider = teiAttributesProvider; - } - - @Override - public Event updateState(Event eventModel, EventStatus newStatus) { - - try { - d2.eventModule().events().uid(eventModel.uid()).setStatus(newStatus); - } catch (D2Error d2Error) { - Timber.e(d2Error); - } - - return d2.eventModule().events().uid(eventModel.uid()).blockingGet(); - } - - @Override - public Observable> getProgramStages(String programUid) { - return d2.programModule().programStages().byProgramUid().eq(programUid).get().toObservable(); - } - - @Override - public Observable getEnrollment() { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).get().toObservable(); - } - - @Override - public Observable> getTEIEnrollmentEvents(String programUid, String teiUid) { - - return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) - .byDeleted().isFalse() - .orderByTimeline(RepositoryScope.OrderByDirection.ASC) - .get().toFlowable().flatMapIterable(events -> events).map(event -> { - if (Boolean.FALSE - .equals(d2.programModule().programs().uid(programUid).blockingGet().ignoreOverdueEvents())) - if (event.status() == EventStatus.SCHEDULE - && event.dueDate().before(DateUtils.getInstance().getToday())) - event = updateState(event, EventStatus.OVERDUE); - - return event; - }).toList() - .toObservable(); - } - - @Override - public Observable> getEnrollmentEventsWithDisplay(String programUid, String teiUid) { - return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).get() - .toObservable() - .map(events -> { - List finalEvents = new ArrayList<>(); - for (Event event : events) { - if (d2.programModule().programStages().uid(event.programStage()).blockingGet().displayGenerateEventBox()) { - finalEvents.add(event); - } - } - return finalEvents; - }); - } - - @Override - public Observable displayGenerateEvent(String eventUid) { - return d2.eventModule().events().uid(eventUid).get().map(Event::programStage) - .flatMap(stageUid -> d2.programModule().programStages().uid(stageUid).get()).toObservable(); - } - - @Override - public Observable>> relationshipsForTeiType(String teType) { - return d2.systemInfoModule().systemInfo().get().toObservable() - .map(SystemInfo::version) - .flatMap(version -> { - if (version.equals("2.29")) - return d2.relationshipModule().relationshipTypes().get().toObservable() - .flatMapIterable(list -> list) - .map(relationshipType -> Pair.create(relationshipType, teType)).toList().toObservable(); - else - return d2.relationshipModule().relationshipTypes().withConstraints().get() - .map(relationshipTypes -> { - List> relTypeList = new ArrayList<>(); - for (RelationshipType relationshipType : relationshipTypes) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint().trackedEntityType() != null && - relationshipType.fromConstraint().trackedEntityType().uid().equals(teType)) { - if (relationshipType.toConstraint() != null && relationshipType.toConstraint().trackedEntityType() != null) { - relTypeList.add(Pair.create(relationshipType, relationshipType.toConstraint().trackedEntityType().uid())); - } - } else if (relationshipType.bidirectional() && relationshipType.toConstraint() != null && relationshipType.toConstraint().trackedEntityType() != null && - relationshipType.toConstraint().trackedEntityType().uid().equals(teType)) { - if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint().trackedEntityType() != null) { - relTypeList.add(Pair.create(relationshipType, relationshipType.fromConstraint().trackedEntityType().uid())); - } - } - } - return relTypeList; - }).toObservable(); - }); - } - - @Override - public CategoryOptionCombo catOptionCombo(String catComboUid) { - return d2.categoryModule().categoryOptionCombos().uid(catComboUid).blockingGet(); - } - - public Observable>> getAttributesMap(String programUid, String teiUid) { - return teiAttributesProvider.getProgramTrackedEntityAttributesByProgram(programUid, teiUid) - .toObservable() - .flatMapIterable(list -> list) - .map(pair -> { - TrackedEntityAttribute attribute = pair.getFirst(); - TrackedEntityAttributeValue attributeValue = pair.getSecond(); - - TrackedEntityAttributeValue formattedAttributeValue; - - if (attributeValue != null && attribute.valueType() != ValueType.IMAGE) { - formattedAttributeValue = ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null); - } else { - formattedAttributeValue = TrackedEntityAttributeValue.builder() - .trackedEntityAttribute(attribute.uid()) - .trackedEntityInstance(teiUid) - .value("") - .build(); - } - return Pair.create(attribute, formattedAttributeValue); - }).toList().toObservable(); - } - - @Override - public Observable> getTEIAttributeValues(String programUid, String teiUid) { - if (programUid != null) { - return teiAttributesProvider.getValuesFromProgramTrackedEntityAttributesByProgram(programUid, teiUid) - .map(attributesValues -> { - List formattedValues = new ArrayList<>(); - for (TrackedEntityAttributeValue attributeValue : attributesValues) { - if (attributeValue.value() != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - if (attribute.valueType() != ValueType.IMAGE) { - formattedValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } else { - formattedValues.add( - TrackedEntityAttributeValue.builder() - .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) - .trackedEntityInstance(teiUid) - .value("") - .build() - ); - } - } - return formattedValues; - }).toObservable(); - - } else { - String teType = d2.trackedEntityModule().trackedEntityInstances().uid(teiUid).blockingGet().trackedEntityType(); - List attributeValues = new ArrayList<>(); - - for (TrackedEntityAttributeValue attributeValue: teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes(teType, teiUid)) { - if (attributeValue != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - if (attribute.valueType() != ValueType.IMAGE && attributeValue.value() != null) { - attributeValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } - } - - if (attributeValues.isEmpty()) { - for (TrackedEntityAttributeValue attributeValue: teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes(teType, teiUid)) { - if (attributeValue != null) { - TrackedEntityAttribute attribute = d2.trackedEntityModule().trackedEntityAttributes().uid(attributeValue.trackedEntityAttribute()).blockingGet(); - attributeValues.add( - ValueUtils.transform(d2, attributeValue, attribute.valueType(), attribute.optionSet() != null ? attribute.optionSet().uid() : null) - ); - } - } - } - return Observable.just(attributeValues); - } - } - - @Override - public boolean setFollowUp(String enrollmentUid) { - - boolean followUp = Boolean.TRUE - .equals(d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().followUp()); - try { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(!followUp); - return !followUp; - } catch (D2Error d2Error) { - Timber.e(d2Error); - return followUp; - } - } - - @Override - public Flowable completeEnrollment(@NonNull String enrollmentUid) { - return Flowable.fromCallable(() -> { - d2.enrollmentModule().enrollments().uid(enrollmentUid) - .setStatus(EnrollmentStatus.COMPLETED); - return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet(); - }); - } - - @Override - public Observable getTrackedEntityInstance(String teiUid) { - return Observable.fromCallable( - () -> d2.trackedEntityModule().trackedEntityInstances().byUid().eq(teiUid).one().blockingGet()); - } - - @Override - public Observable> getProgramTrackedEntityAttributes(String programUid) { - if (programUid != null) { - return d2.programModule().programTrackedEntityAttributes().byProgram().eq(programUid) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).get().toObservable(); - } else { - return Observable.fromCallable(() -> d2.trackedEntityModule().trackedEntityAttributes() - .byDisplayInListNoProgram().eq(true).blockingGet()).map(trackedEntityAttributes -> { - List programs = d2.programModule().programs().blockingGet(); - - List teaUids = UidsHelper.getUidsList(trackedEntityAttributes); - List programTrackedEntityAttributes = new ArrayList<>(); - - for (Program program : programs) { - List attributeList = d2.programModule() - .programTrackedEntityAttributes().byProgram().eq(program.uid()) - .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet(); - - for (ProgramTrackedEntityAttribute pteattr : attributeList) { - if (teaUids.contains(pteattr.uid())) - programTrackedEntityAttributes.add(pteattr); - } - } - return programTrackedEntityAttributes; - }); - } - } - - @Override - public Observable> getTeiOrgUnits(@NonNull String teiUid, @Nullable String programUid) { - EnrollmentCollectionRepository enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() - .eq(teiUid); - if (programUid != null) { - enrollmentRepo = enrollmentRepo.byProgram().eq(programUid); - } - - return enrollmentRepo.get().toObservable().map(enrollments -> { - List orgUnitIds = new ArrayList<>(); - for (Enrollment enrollment : enrollments) { - orgUnitIds.add(enrollment.organisationUnit()); - } - return d2.organisationUnitModule().organisationUnits().byUid().in(orgUnitIds).blockingGet(); - }); - } - - @Override - public Observable> getTeiActivePrograms(String teiUid, boolean showOnlyActive) { - EnrollmentCollectionRepository enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() - .eq(teiUid).byDeleted().eq(false); - if (showOnlyActive) - enrollmentRepo.byStatus().eq(EnrollmentStatus.ACTIVE); - return enrollmentRepo.get().toObservable().flatMapIterable(enrollments -> enrollments) - .map(Enrollment::program).toList().toObservable() - .map(programUids -> d2.programModule().programs().byUid().in(programUids).blockingGet()); - } - - @Override - public Observable> getTEIEnrollments(String teiUid) { - return d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid).byDeleted().eq(false).get().toObservable(); - } - - @Override - public void saveCatOption(String eventUid, String catOptionComboUid) { - try { - d2.eventModule().events().uid(eventUid).setAttributeOptionComboUid(catOptionComboUid); - } catch (D2Error d2Error) { - Timber.e(d2Error); - } - } - - @Override - public Single deleteTeiIfPossible() { - return Single.fromCallable(() -> { - boolean local = d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .blockingGet() - .state() == State.TO_POST; - boolean hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_TEI_CASCADE_DELETE") - .one().blockingExists(); - return local || hasAuthority; - }).flatMap(canDelete -> { - if (canDelete) { - return d2.trackedEntityModule() - .trackedEntityInstances() - .uid(teiUid) - .delete() - .andThen(Single.fromCallable(() -> true)); - } else { - return Single.fromCallable(() -> false); - } - }); - } - - @Override - public Single deleteEnrollmentIfPossible(String enrollmentUid) { - return Single.fromCallable(() -> { - boolean local = d2.enrollmentModule() - .enrollments() - .uid(enrollmentUid) - .blockingGet().state() == State.TO_POST; - boolean hasAuthority = d2.userModule() - .authorities() - .byName().eq("F_ENROLLMENT_CASCADE_DELETE") - .one().blockingExists(); - return local || hasAuthority; - }).flatMap(canDelete -> { - if (canDelete) { - return Single.fromCallable(() -> { - EnrollmentObjectRepository enrollmentObjectRepository = d2.enrollmentModule() - .enrollments().uid(enrollmentUid); - enrollmentObjectRepository.setStatus( - enrollmentObjectRepository.blockingGet().status() - ); - enrollmentObjectRepository.blockingDelete(); - return !d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) - .byDeleted().isFalse() - .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isEmpty(); - }); - } else { - return Single.error(new AuthorityException(null)); - } - }); - } - - @Override - public Single getNoteCount() { - return d2.enrollmentModule().enrollments() - .withNotes() - .uid(enrollmentUid) - .get() - .map(enrollment -> enrollment.notes() != null ? enrollment.notes().size() : 0); - } - - @Override - public EnrollmentStatus getEnrollmentStatus(String enrollmentUid) { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet().status(); - } - - @Override - public Observable updateEnrollmentStatus(String enrollmentUid, EnrollmentStatus status) { - try { - if (d2.programModule().programs().uid(programUid).blockingGet().access().data().write()) { - if (reopenCheck(status)) { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setStatus(status); - return Observable.just(StatusChangeResultCode.CHANGED); - } else { - return Observable.just(StatusChangeResultCode.ACTIVE_EXIST); - } - } else { - return Observable.just(StatusChangeResultCode.WRITE_PERMISSION_FAIL); - } - } catch (D2Error error) { - return Observable.just(StatusChangeResultCode.FAILED); - } - } - - private boolean reopenCheck(EnrollmentStatus status) { - return status != EnrollmentStatus.ACTIVE || d2.enrollmentModule().enrollments() - .byProgram().eq(programUid) - .byTrackedEntityInstance().eq(teiUid) - .byStatus().eq(EnrollmentStatus.ACTIVE) - .blockingIsEmpty(); - } - - @Override - public boolean programHasRelationships() { - if (programUid != null) { - String teiTypeUid = d2.programModule().programs() - .uid(programUid) - .blockingGet() - .trackedEntityType() - .uid(); - return !relationshipsForTeiType(teiTypeUid).blockingFirst().isEmpty(); - } else { - return false; - } - } - - @Override - public boolean programHasAnalytics() { - if (programUid != null) { - List enrollmentScopeRulesUids = d2.programModule().programRules() - .byProgramUid().eq(programUid) - .byProgramStageUid().isNull() - .blockingGetUids(); - boolean hasDisplayRuleActions = !d2.programModule().programRuleActions() - .byProgramRuleUid().in(enrollmentScopeRulesUids) - .byProgramRuleActionType().in(ProgramRuleActionType.DISPLAYKEYVALUEPAIR, ProgramRuleActionType.DISPLAYTEXT) - .blockingIsEmpty(); - boolean hasProgramIndicator = !d2.programModule().programIndicators().byProgramUid().eq(programUid).blockingIsEmpty(); - boolean hasCharts = charts != null && !charts.geEnrollmentCharts(enrollmentUid).isEmpty(); - return hasDisplayRuleActions || hasProgramIndicator || hasCharts; - } else { - return false; - } - } - - @Override - public String getTETypeName() { - return getTrackedEntityInstance(teiUid).flatMap(tei -> - d2.trackedEntityModule().trackedEntityTypes() - .uid(tei.trackedEntityType()) - .get() - .toObservable() - ).blockingFirst().displayName(); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt new file mode 100644 index 0000000000..30ea10c103 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -0,0 +1,562 @@ +package org.dhis2.usescases.teiDashboard + +import dhis2.org.analytics.charts.Charts +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Function +import org.dhis2.commons.data.tuples.Pair +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.utils.AuthorityException +import org.dhis2.utils.DateUtils +import org.dhis2.utils.ValueUtils +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.category.CategoryOptionCombo +import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramRuleActionType +import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute +import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.android.core.systeminfo.SystemInfo +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute +import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import timber.log.Timber + +class DashboardRepositoryImpl( + private val d2: D2, + private val charts: Charts, + private val teiUid: String, + private val programUid: String, + private val enrollmentUid: String, + private val resources: ResourceManager, + private val teiAttributesProvider: TeiAttributesProvider, +) : DashboardRepository { + override fun getProgramStages(programStages: String): Observable> { + return d2.programModule().programStages().byProgramUid().eq(programUid).get().toObservable() + } + + override fun getEnrollment(): Observable { + return d2.enrollmentModule().enrollments().uid(enrollmentUid).get().map { it }.toObservable() + } + + override fun getTEIEnrollmentEvents( + programUid: String?, + teiUid: String, + ): Observable> { + return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid) + .byDeleted().isFalse + .orderByTimeline(RepositoryScope.OrderByDirection.ASC) + .get().toFlowable().flatMapIterable { events: List? -> events } + .map { event: Event -> + var event = event + if (java.lang.Boolean.FALSE + == d2.programModule().programs().uid(programUid).blockingGet()!! + .ignoreOverdueEvents() + ) if (event.status() == EventStatus.SCHEDULE && + event.dueDate()!! + .before(DateUtils.getInstance().today) + ) { + event = updateState(event, EventStatus.OVERDUE) + } + event + }.toList() + .toObservable() + } + + override fun getEnrollmentEventsWithDisplay( + programUid: String?, + teiUid: String, + ): Observable> { + return d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).get() + .toObservable() + .map { events: List -> + val finalEvents: MutableList = + ArrayList() + for (event in events) { + if (d2.programModule().programStages().uid(event.programStage()).blockingGet()!! + .displayGenerateEventBox()!! + ) { + finalEvents.add(event) + } + } + finalEvents + } + } + + override fun getTEIAttributeValues( + programUid: String?, + teiUid: String, + ): Observable> { + return if (programUid != null) { + teiAttributesProvider.getValuesFromProgramTrackedEntityAttributesByProgram( + programUid, + teiUid, + ) + .map> { attributesValues: List -> + val formattedValues: MutableList = + java.util.ArrayList() + for (attributeValue in attributesValues) { + if (attributeValue.value() != null) { + val attribute = + d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE) { + formattedValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute!!.valueType(), + if (attribute!!.optionSet() != null) { + attribute!!.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } else { + formattedValues.add( + TrackedEntityAttributeValue.builder() + .trackedEntityAttribute(attributeValue.trackedEntityAttribute()) + .trackedEntityInstance(teiUid) + .value("") + .build(), + ) + } + } + formattedValues + }.toObservable() + } else { + val teType = + d2.trackedEntityModule().trackedEntityInstances().uid(teiUid).blockingGet()!! + .trackedEntityType() + val attributeValues: MutableList = java.util.ArrayList() + for (attributeValue in teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes( + teType, + teiUid, + )) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + if (attribute!!.valueType() != ValueType.IMAGE && attributeValue.value() != null) { + attributeValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } + if (attributeValues.isEmpty()) { + for (attributeValue in teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes( + teType, + teiUid, + )) { + val attribute = d2.trackedEntityModule().trackedEntityAttributes() + .uid(attributeValue.trackedEntityAttribute()).blockingGet() + attributeValues.add( + ValueUtils.transform( + d2, + attributeValue, + attribute!!.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ), + ) + } + } + Observable.just(attributeValues) + } + } + + override fun setFollowUp(enrollmentUid: String?): Boolean { + val followUp = ( + java.lang.Boolean.TRUE + == d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! + .followUp() + ) + return try { + d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(!followUp) + !followUp + } catch (d2Error: D2Error) { + Timber.e(d2Error) + followUp + } + } + + override fun updateState(event: Event?, newStatus: EventStatus): Event { + try { + d2.eventModule().events().uid(event?.uid()).setStatus(newStatus) + } catch (d2Error: D2Error) { + Timber.e(d2Error) + } + return d2.eventModule().events().uid(event?.uid()).blockingGet()!! + } + + override fun completeEnrollment(enrollmentUid: String): Flowable { + return Flowable.fromCallable { + d2.enrollmentModule().enrollments().uid(enrollmentUid) + .setStatus(EnrollmentStatus.COMPLETED) + d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet() + } + } + + override fun displayGenerateEvent(eventUid: String?): Observable { + return d2.eventModule().events().uid(eventUid).get() + .map { obj: Event -> obj.programStage() } + .flatMap { stageUid: String -> + d2.programModule().programStages().uid(stageUid).get() + }.map { it }.toObservable() + } + + override fun relationshipsForTeiType(teType: String): Observable>> { + return d2.systemInfoModule().systemInfo().get().toObservable() + .map(Function { obj: SystemInfo -> obj.version() }) + .flatMap { version: String? -> + if (version == "2.29") { + return@flatMap d2.relationshipModule().relationshipTypes() + .get().toObservable() + .flatMapIterable { list: List? -> list } + .map> { relationshipType: RelationshipType? -> + Pair.create( + relationshipType!!, + teType, + ) + }.toList().toObservable() + } else { + return@flatMap d2.relationshipModule() + .relationshipTypes().withConstraints().get() + .map>> { relationshipTypes: List -> + val relTypeList: MutableList> = + java.util.ArrayList() + for (relationshipType in relationshipTypes) { + if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! + .trackedEntityType() != null && relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid() == teType + ) { + if (relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.toConstraint()!! + .trackedEntityType()!!.uid(), + ), + ) + } + } else if (relationshipType.bidirectional()!! && relationshipType.toConstraint() != null && relationshipType.toConstraint()!! + .trackedEntityType() != null && relationshipType.toConstraint()!! + .trackedEntityType()!! + .uid() == teType + ) { + if (relationshipType.fromConstraint() != null && relationshipType.fromConstraint()!! + .trackedEntityType() != null + ) { + relTypeList.add( + Pair.create( + relationshipType, + relationshipType.fromConstraint()!! + .trackedEntityType()!!.uid(), + ), + ) + } + } + } + relTypeList.toList() + }.toObservable() + } + } + } + + override fun catOptionCombo(catComboUid: String?): CategoryOptionCombo { + return d2.categoryModule().categoryOptionCombos().uid(catComboUid).blockingGet()!! + } + + override fun getTrackedEntityInstance(teiUid: String): Observable { + return Observable.fromCallable { + d2.trackedEntityModule().trackedEntityInstances().byUid().eq(teiUid).one() + .blockingGet() + } + } + + override fun getProgramTrackedEntityAttributes(programUid: String?): Observable> { + return if (programUid != null) { + d2.programModule().programTrackedEntityAttributes().byProgram().eq(programUid) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).get().toObservable() + } else { + Observable.fromCallable { + d2.trackedEntityModule().trackedEntityAttributes() + .byDisplayInListNoProgram().eq(true).blockingGet() + }.map { trackedEntityAttributes: List -> + val programs = + d2.programModule().programs().blockingGet() + val teaUids = getUidsList(trackedEntityAttributes) + val programTrackedEntityAttributes: MutableList = + java.util.ArrayList() + for (program in programs) { + val attributeList = + d2.programModule() + .programTrackedEntityAttributes().byProgram().eq(program.uid()) + .orderBySortOrder(RepositoryScope.OrderByDirection.ASC).blockingGet() + for (pteattr in attributeList) { + if (teaUids.contains(pteattr.uid())) { + programTrackedEntityAttributes.add( + pteattr, + ) + } + } + } + programTrackedEntityAttributes + } + } + } + + override fun getTeiOrgUnits( + teiUid: String, + programUid: String?, + ): Observable> { + var enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() + .eq(teiUid) + if (programUid != null) { + enrollmentRepo = enrollmentRepo.byProgram().eq(programUid) + } + + return enrollmentRepo.get().toObservable().map { enrollments: List -> + val orgUnitIds: MutableList = + java.util.ArrayList() + for (enrollment in enrollments) { + enrollment.organisationUnit()?.let { orgUnitIds.add(it) } + } + d2.organisationUnitModule().organisationUnits().byUid().`in`(orgUnitIds.toList()) + .blockingGet() + } + } + + override fun getTeiActivePrograms( + teiUid: String, + showOnlyActive: Boolean, + ): Observable> { + val enrollmentRepo = d2.enrollmentModule().enrollments().byTrackedEntityInstance() + .eq(teiUid).byDeleted().eq(false) + if (showOnlyActive) enrollmentRepo.byStatus().eq(EnrollmentStatus.ACTIVE) + + return enrollmentRepo.get().toObservable() + .flatMapIterable { enrollments: List? -> enrollments } + .map { obj: Enrollment -> obj.program() } + .toList().toObservable() + .map { programUids -> + d2.programModule().programs().byUid() + .`in`(programUids.filterNotNull()).blockingGet() + } + } + + override fun getTEIEnrollments(teiUid: String): Observable> { + return d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid).byDeleted() + .eq(false).get().toObservable() + } + + override fun saveCatOption(eventUid: String?, catOptionComboUid: String?) { + try { + d2.eventModule().events().uid(eventUid).setAttributeOptionComboUid(catOptionComboUid) + } catch (d2Error: D2Error) { + Timber.e(d2Error) + } + } + + override fun deleteTeiIfPossible(): Single { + return Single.fromCallable { + val local = d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .blockingGet() + ?.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_TEI_CASCADE_DELETE") + .one().blockingExists() + local || hasAuthority + }.flatMap { canDelete: Boolean -> + if (canDelete) { + return@flatMap d2.trackedEntityModule() + .trackedEntityInstances() + .uid(teiUid) + .delete() + .andThen(Single.fromCallable { true }) + } else { + return@flatMap Single.fromCallable { false } + } + } + } + + override fun deleteEnrollmentIfPossible(enrollmentUid: String): Single { + return Single.fromCallable { + val local = d2.enrollmentModule() + .enrollments() + .uid(enrollmentUid) + .blockingGet()!!.state() == State.TO_POST + val hasAuthority = d2.userModule() + .authorities() + .byName().eq("F_ENROLLMENT_CASCADE_DELETE") + .one().blockingExists() + local || hasAuthority + }.flatMap { canDelete: Boolean -> + if (canDelete) { + return@flatMap Single.fromCallable { + val enrollmentObjectRepository = d2.enrollmentModule() + .enrollments().uid(enrollmentUid) + enrollmentObjectRepository.setStatus( + enrollmentObjectRepository.blockingGet()!!.status()!!, + ) + enrollmentObjectRepository.blockingDelete() + d2.enrollmentModule().enrollments().byTrackedEntityInstance().eq(teiUid) + .byDeleted().isFalse + .byStatus().eq(EnrollmentStatus.ACTIVE).blockingGet().isNotEmpty() + } + } else { + return@flatMap Single.error(AuthorityException(null)) + } + } + } + + override fun getNoteCount(): Single { + return d2.enrollmentModule().enrollments() + .withNotes() + .uid(enrollmentUid) + .get() + .map(Function { enrollment: Enrollment -> if (enrollment.notes() != null) enrollment.notes()!!.size else 0 }) + } + + override fun getEnrollmentStatus(enrollmentUid: String?): EnrollmentStatus? { + return d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!!.status() + } + + override fun updateEnrollmentStatus( + enrollmentUid: String, + status: EnrollmentStatus, + ): Observable { + return try { + if (d2.programModule().programs().uid(programUid).blockingGet()!!.access().data() + .write() + ) { + if (reopenCheck(status)) { + d2.enrollmentModule().enrollments().uid(enrollmentUid).setStatus(status) + Observable.just(StatusChangeResultCode.CHANGED) + } else { + Observable.just(StatusChangeResultCode.ACTIVE_EXIST) + } + } else { + Observable.just(StatusChangeResultCode.WRITE_PERMISSION_FAIL) + } + } catch (error: D2Error) { + Observable.just(StatusChangeResultCode.FAILED) + } + } + + private fun reopenCheck(status: EnrollmentStatus): Boolean { + return status != EnrollmentStatus.ACTIVE || d2.enrollmentModule().enrollments() + .byProgram().eq(programUid) + .byTrackedEntityInstance().eq(teiUid) + .byStatus().eq(EnrollmentStatus.ACTIVE) + .blockingIsEmpty() + } + + override fun programHasRelationships(): Boolean { + return if (programUid != null) { + val teiTypeUid = d2.programModule().programs() + .uid(programUid) + .blockingGet() + ?.trackedEntityType() + ?.uid() + teiTypeUid?.let { relationshipsForTeiType(it) }!!.blockingFirst().isNotEmpty() + } else { + false + } + } + + override fun programHasAnalytics(): Boolean { + return if (programUid != null) { + val enrollmentScopeRulesUids = d2.programModule().programRules() + .byProgramUid().eq(programUid) + .byProgramStageUid().isNull + .blockingGetUids() + val hasDisplayRuleActions = !d2.programModule().programRuleActions() + .byProgramRuleUid().`in`(enrollmentScopeRulesUids) + .byProgramRuleActionType() + .`in`(ProgramRuleActionType.DISPLAYKEYVALUEPAIR, ProgramRuleActionType.DISPLAYTEXT) + .blockingIsEmpty() + val hasProgramIndicator = + !d2.programModule().programIndicators().byProgramUid().eq(programUid) + .blockingIsEmpty() + val hasCharts = charts.geEnrollmentCharts(enrollmentUid).isNotEmpty() + hasDisplayRuleActions || hasProgramIndicator || hasCharts + } else { + false + } + } + + override fun getTETypeName(): String? { + return getTrackedEntityInstance(teiUid).flatMap { tei: TrackedEntityInstance -> + d2.trackedEntityModule().trackedEntityTypes() + .uid(tei.trackedEntityType()) + .get() + .toObservable() + }.blockingFirst()?.displayName() + } + + override fun getAttributesMap( + programUid: String, + teiUid: String, + ): Observable>> { + return teiAttributesProvider.getProgramTrackedEntityAttributesByProgram(programUid, teiUid) + .toObservable() + .flatMapIterable { list: List>? -> list } + .map { (attribute, attributeValue): kotlin.Pair -> + val formattedAttributeValue: TrackedEntityAttributeValue = if (attributeValue != null && attribute!!.valueType() != ValueType.IMAGE) { + ValueUtils.transform( + d2, + attributeValue, + attribute.valueType(), + if (attribute.optionSet() != null) { + attribute.optionSet()!! + .uid() + } else { + null + }, + ) + } else { + TrackedEntityAttributeValue.builder() + .trackedEntityAttribute(attribute!!.uid()) + .trackedEntityInstance(teiUid) + .value("") + .build() + } + Pair.create( + attribute, + formattedAttributeValue, + ) + }.toList().toObservable() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt index fda6a377e7..b6ddd5bcde 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -145,9 +145,9 @@ class TeiDashboardMobileActivity : (applicationContext as App).createDashboardComponent( TeiDashboardModule( this, - teiUid, - programUid, - enrollmentUid, + teiUid ?: "", + programUid ?: "", + enrollmentUid ?: "", this.isPortrait(), ), ).inject(this) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt index 35109ba13d..da61839c2e 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt @@ -20,9 +20,9 @@ import org.hisp.dhis.android.core.D2 @Module class TeiDashboardModule( private val view: TeiDashboardContracts.View, - val teiUid: String?, - val programUid: String?, - private val enrollmentUid: String?, + val teiUid: String, + val programUid: String, + private val enrollmentUid: String, private val isPortrait: Boolean, ) { @Provides diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt index 0daf8d62be..a9ba0d848f 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt @@ -12,6 +12,12 @@ import org.dhis2.utils.dialFloatingActionButton.DialItem class EventCreationOptionsMapper(val resources: ResourceManager) { + companion object { + const val REFERAL_ID = 3 + const val ADD_NEW_ID = 2 + const val SCHEDULE_ID = 1 + } + fun mapToEventsByStage(availableOptions: List): List { return availableOptions.map { item -> EventCreationOptions( @@ -40,12 +46,25 @@ class EventCreationOptionsMapper(val resources: ResourceManager) { } } + fun getActionType(eventCreationId: Int): EventCreationType { + return when (eventCreationId) { + SCHEDULE_ID -> SCHEDULE + ADD_NEW_ID -> ADDNEW + REFERAL_ID -> REFERAL + else -> throw UnsupportedOperationException( + "id %s is not supported as an event creation".format( + eventCreationId, + ), + ) + } + } + private fun getItemId(item: EventCreationType): Int { return when (item) { - SCHEDULE -> TEIDataFragment.SCHEDULE_ID - ADDNEW -> TEIDataFragment.ADD_NEW_ID - REFERAL -> TEIDataFragment.REFERAL_ID - DEFAULT -> TEIDataFragment.ADD_NEW_ID + SCHEDULE -> SCHEDULE_ID + ADDNEW -> ADD_NEW_ID + REFERAL -> REFERAL_ID + DEFAULT -> ADD_NEW_ID } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt index ea2116b6fa..3a81eb28c7 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata import android.content.Intent import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LifecycleOwner import io.reactivex.Flowable import io.reactivex.Single import io.reactivex.functions.Consumer @@ -20,14 +21,14 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance class TEIDataContracts { interface View : AbstractActivityContracts.View { - fun setEvents(events: List, canAddEvents: Boolean) + fun viewLifecycleOwner(): LifecycleOwner + fun setEvents(events: List) fun displayGenerateEvent(): Consumer fun areEventsCompleted(): Consumer> fun enrollmentCompleted(): Consumer fun switchFollowUp(followUp: Boolean) fun displayGenerateEvent(eventUid: String) fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) - fun seeDetails(intent: Intent, options: ActivityOptionsCompat) fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) fun openEventInitial(intent: Intent) fun openEventCapture(intent: Intent) @@ -50,7 +51,7 @@ class TEIDataContracts { fun showSyncDialog(eventUid: String, enrollmentUid: String) fun displayCatComboOptionSelectorForEvents(data: List) - fun showProgramRuleErrorMessage(message: String) + fun showProgramRuleErrorMessage() fun showCatOptComboDialog(catComboUid: String) fun goToEventInitial(eventCreationType: EventCreationType, programStage: ProgramStage) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt index 22e458dbf3..145ef18362 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt @@ -8,12 +8,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.app.ActivityOptionsCompat import androidx.databinding.ObservableBoolean import androidx.fragment.app.activityViewModels +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DividerItemDecoration import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.CircleCrop @@ -93,6 +93,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { @Inject lateinit var infoBarMapper: InfoBarMapper + @Inject + lateinit var contractHandler: TeiDataContractHandler + private var eventAdapter: EventAdapter? = null private var dialog: CustomDialog? = null private var programStageFromEvent: ProgramStage? = null @@ -102,29 +105,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { private lateinit var dashboardModel: DashboardProgramModel private val dashboardActivity: TeiDashboardMobileActivity by lazy { context as TeiDashboardMobileActivity } - private val detailsLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { - dashboardActivity.presenter.init() - } - - private val eventCreationLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventCaptureLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventDetailsLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - private val eventInitialLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - dashboardActivity.presenter.init() - } - override fun onAttach(context: Context) { super.onAttach(context) with(requireArguments()) { @@ -138,6 +118,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { programUid, teiUid, enrollmentUid, + requireActivity().activityResultRegistry, ), )?.inject(this@TEIDataFragment) } @@ -153,13 +134,19 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { binding.presenter = presenter dashboardActivity.observeGrouping()?.observe(viewLifecycleOwner) { group -> showLoadingProgress(true) - binding.isGrouping = group presenter.onGroupingChanged(group) } dashboardActivity.observeFilters()?.observe(viewLifecycleOwner, ::showHideFilters) dashboardActivity.updatedEnrollment()?.observe(viewLifecycleOwner, ::updateEnrollment) binding.filterLayout.adapter = filtersAdapter + presenter.shouldDisplayEventCreationButton.observe(this.viewLifecycleOwner) { showCreateEventButton -> + binding.dialFabLayout.visibility = if (showCreateEventButton) { + View.VISIBLE.also { binding.dialFabLayout.setFabVisible(true) } + } else { + View.GONE.also { binding.dialFabLayout.setFabVisible(false) } + } + } }.root } @@ -169,14 +156,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { private fun updateFabItems() { val dialItems = presenter.newEventOptionsByTimeline() - binding.dialFabLayout.addDialItems(dialItems) { clickedId: Int? -> - when (clickedId) { - REFERAL_ID -> createEvent(EventCreationType.REFERAL, 0) - ADD_NEW_ID -> createEvent(EventCreationType.ADDNEW, 0) - SCHEDULE_ID -> createEvent(EventCreationType.SCHEDULE, 0) - else -> {} - } - } + binding.dialFabLayout.addDialItems(dialItems, presenter::onEventCreationClick) } override fun setEnrollment(enrollment: Enrollment) { @@ -228,7 +208,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { fun setData(dashboardModel: DashboardProgramModel) { this.dashboardModel = dashboardModel if (dashboardModel.currentEnrollment != null) { - binding.dialFabLayout.setFabVisible(true) +// binding.dialFabLayout.setFabVisible(true) presenter.setDashboardProgram(dashboardModel) eventCatComboOptionSelector = EventCatComboOptionSelector( dashboardModel.currentProgram.categoryComboUid(), @@ -298,7 +278,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { ) } } else { - binding.dialFabLayout.setFabVisible(false) +// binding.dialFabLayout.setFabVisible(false) binding.teiRecycler.adapter = DashboardProgramAdapter(presenter, dashboardModel) binding.teiRecycler.addItemDecoration( DividerItemDecoration( @@ -327,6 +307,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { Intent.ACTION_DIAL -> { data = Uri.parse("tel:$value") } + Intent.ACTION_SENDTO -> { data = Uri.parse("mailto:$value") } @@ -359,8 +340,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { return eventAdapter?.stageSelector() ?: Flowable.empty() } - override fun setEvents(events: List, canAddEvents: Boolean) { - binding.canAddEvents = canAddEvents + override fun setEvents(events: List) { if (events.isEmpty()) { binding.emptyTeis.visibility = View.VISIBLE if (binding.dialFabLayout.isFabVisible()) { @@ -401,9 +381,8 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { RC_GENERATE_EVENT, object : DialogClickListener { override fun onPositive() { - createEvent( - EventCreationType.SCHEDULE, - if (programStageFromEvent?.standardInterval() != null) programStageFromEvent?.standardInterval() else 0, + presenter.onAcceptScheduleNewEvent( + programStageModel.standardInterval() ?: 0, ) } @@ -484,10 +463,16 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, scheduleIntervalDays ?: 0) val intent = Intent(context, ProgramStageSelectionActivity::class.java) intent.putExtras(bundle) - eventCreationLauncher.launch(intent) + contractHandler.createEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } } } + override fun viewLifecycleOwner(): LifecycleOwner { + return this.viewLifecycleOwner + } + override fun switchFollowUp(followUp: Boolean) { this.followUp.set(followUp) } @@ -509,15 +494,20 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { dashboardActivity.finish() } - override fun seeDetails(intent: Intent, options: ActivityOptionsCompat) = - detailsLauncher.launch(intent, options) - override fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) = - eventDetailsLauncher.launch(intent, options) + contractHandler.scheduleEvent(intent, options).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } - override fun openEventInitial(intent: Intent) = eventInitialLauncher.launch(intent) + override fun openEventInitial(intent: Intent) = + contractHandler.editEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } - override fun openEventCapture(intent: Intent) = eventCaptureLauncher.launch(intent) + override fun openEventCapture(intent: Intent) = + contractHandler.editEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } override fun showTeiImage(filePath: String, defaultIcon: String) { if (filePath.isEmpty() && defaultIcon.isEmpty()) { @@ -571,7 +561,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { bundle.putString(Constants.PROGRAM_STAGE_UID, programStage.uid()) bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, programStage.standardInterval() ?: 0) intent.putExtras(bundle) - eventInitialLauncher.launch(intent) + contractHandler.createEvent(intent).observe(this.viewLifecycleOwner) { + dashboardActivity.presenter.init() + } } private fun showHideFilters(showFilters: Boolean) { @@ -635,8 +627,10 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { eventCatComboOptionSelector?.requestCatComboOption(presenter::changeCatOption) } - override fun showProgramRuleErrorMessage(message: String) { - dashboardActivity.runOnUiThread { showDescription(message) } + override fun showProgramRuleErrorMessage() { + dashboardActivity.runOnUiThread { + showDescription(getString(R.string.error_applying_rule_effects)) + } } override fun showCatOptComboDialog(catComboUid: String) { @@ -656,9 +650,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { companion object { const val RC_GENERATE_EVENT = 1501 const val RC_EVENTS_COMPLETED = 1601 - const val REFERAL_ID = 3 - const val ADD_NEW_ID = 2 - const val SCHEDULE_ID = 1 const val PREF_COMPLETED_EVENT = "COMPLETED_EVENT" @JvmStatic diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt index 4c601f9be7..2dcb1d19d6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt @@ -1,5 +1,6 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata +import androidx.activity.result.ActivityResultRegistry import dagger.Module import dagger.Provides import org.dhis2.commons.data.EntryMode @@ -21,6 +22,7 @@ import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts import org.dhis2.usescases.teiDashboard.data.ProgramConfigurationRepository import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper @@ -34,6 +36,7 @@ class TEIDataModule( private val programUid: String?, private val teiUid: String, private val enrollmentUid: String, + private val registry: ActivityResultRegistry, ) { @Provides @PerFragment @@ -48,10 +51,11 @@ class TEIDataModule( filterManager: FilterManager, filterRepository: FilterRepository, valueStore: FormValueStore, - resources: ResourceManager, optionsRepository: OptionsRepository, getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions, eventCreationOptionsMapper: EventCreationOptionsMapper, + contractHandler: TeiDataContractHandler, + dashboardActivityPresenter: TeiDashboardContracts.Presenter, ): TEIDataPresenter { return TEIDataPresenter( view, @@ -68,10 +72,11 @@ class TEIDataModule( filterManager, filterRepository, valueStore, - resources, optionsRepository, getNewEventCreationTypeOptions, eventCreationOptionsMapper, + contractHandler, + dashboardActivityPresenter, ) } @@ -152,4 +157,7 @@ class TEIDataModule( ): InfoBarMapper { return InfoBarMapper(resourceManager) } + + @Provides + fun provideContractHandler() = TeiDataContractHandler(registry) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index cb65dff8a3..a8e3aac6f0 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -1,9 +1,12 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata import android.content.Intent +import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.google.gson.reflect.TypeToken import io.reactivex.Flowable import io.reactivex.Observable @@ -12,6 +15,8 @@ import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.BehaviorProcessor import org.dhis2.R import org.dhis2.bindings.profilePicturePath +import org.dhis2.commons.Constants +import org.dhis2.commons.bindings.canCreateEventInEnrollment import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.bindings.event import org.dhis2.commons.bindings.program @@ -22,7 +27,6 @@ import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.forms.dataentry.RuleEngineRepository import org.dhis2.form.data.FormValueStore @@ -32,8 +36,10 @@ import org.dhis2.usescases.events.ScheduledEventActivity.Companion.getIntent import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity.Companion.getActivityBundle import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity +import org.dhis2.usescases.programStageSelection.ProgramStageSelectionActivity import org.dhis2.usescases.teiDashboard.DashboardProgramModel import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.decrement import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.increment import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions @@ -42,7 +48,9 @@ import org.dhis2.utils.EventMode import org.dhis2.utils.Result import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.analytics.CREATE_EVENT_TEI import org.dhis2.utils.analytics.FOLLOW_UP +import org.dhis2.utils.analytics.TYPE_EVENT_TEI import org.dhis2.utils.dialFloatingActionButton.DialItem import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -68,10 +76,11 @@ class TEIDataPresenter( private val filterManager: FilterManager, private val filterRepository: FilterRepository, private val valueStore: FormValueStore, - private val resources: ResourceManager, private val optionsRepository: OptionsRepository, private val getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions, private val eventCreationOptionsMapper: EventCreationOptionsMapper, + private val contractHandler: TeiDataContractHandler, + private val dashboardActivityPresenter: TeiDashboardContracts.Presenter, ) { private val groupingProcessor: BehaviorProcessor = BehaviorProcessor.create() private val compositeDisposable: CompositeDisposable = CompositeDisposable() @@ -79,6 +88,10 @@ class TEIDataPresenter( private var currentStage: String = "" private var stagesToHide: List = emptyList() + private val _groupEvents = MutableLiveData(false) + private val _shouldDisplayEventCreationButton = MutableLiveData(false) + val shouldDisplayEventCreationButton: LiveData = _shouldDisplayEventCreationButton + fun init() { compositeDisposable.add( filterManager.asFlowable().startWith(filterManager) @@ -127,9 +140,10 @@ class TEIDataPresenter( currentStage = if (stageUid == currentStage && !showOptions) "" else stageUid StageSection(currentStage, showOptions) } - val groupingFlowable = groupingProcessor.startWith( - hasGrouping(it), - ) + val programHasGrouping = hasGrouping(it) + val groupingFlowable = groupingProcessor.startWith(programHasGrouping) + _groupEvents.value = programHasGrouping + compositeDisposable.add( Flowable.combineLatest( filterManager.asFlowable().startWith(filterManager), @@ -158,7 +172,7 @@ class TEIDataPresenter( .observeOn(schedulerProvider.ui()) .subscribe( { events -> - view.setEvents(events, canAddNewEvents()) + view.setEvents(events) decrement() }, Timber.Forest::d, @@ -188,7 +202,12 @@ class TEIDataPresenter( Timber.Forest::e, ), ) - } ?: view.setEnrollmentData(null, null) + } ?: run { + view.setEnrollmentData(null, null) + _shouldDisplayEventCreationButton.value = false + } + + updateCreateEventButtonVisibility(_groupEvents.value ?: false) compositeDisposable.add( Single.zip( @@ -229,6 +248,16 @@ class TEIDataPresenter( ) } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun updateCreateEventButtonVisibility(isGrouping: Boolean) { + val enrollment = d2.enrollment(enrollmentUid) + val showButton = + enrollment != null && + !isGrouping && enrollment.status() == EnrollmentStatus.ACTIVE && + canAddNewEvents() + _shouldDisplayEventCreationButton.value = showButton + } + private fun hasGrouping(programUid: String): Boolean { var hasGrouping = true if (grouping.containsKey(programUid)) { @@ -244,9 +273,7 @@ class TEIDataPresenter( Timber.d("APPLYING EFFECTS") if (calcResult.error() != null) { Timber.e(calcResult.error()) - view.showProgramRuleErrorMessage( - resources.getString(R.string.error_applying_rule_effects), - ) + view.showProgramRuleErrorMessage() return emptyList() } val (_, _, _, _, _, _, stagesToHide1) = RulesUtilsProviderImpl( @@ -353,6 +380,40 @@ class TEIDataPresenter( view.switchFollowUp(followup) } + fun onEventCreationClick(eventCreationId: Int) { + createEventInEnrollment(eventCreationOptionsMapper.getActionType(eventCreationId)) + } + + fun onAcceptScheduleNewEvent(stageStandardInterval: Int) { + createEventInEnrollment(EventCreationType.SCHEDULE, stageStandardInterval) + } + + private fun createEventInEnrollment( + eventCreationType: EventCreationType, + scheduleIntervalDays: Int = 0, + ) { + analyticsHelper.setEvent(TYPE_EVENT_TEI, eventCreationType.name, CREATE_EVENT_TEI) + val bundle = Bundle() + bundle.putString( + Constants.PROGRAM_UID, + dashboardModel?.currentEnrollment?.program(), + ) + bundle.putString(Constants.TRACKED_ENTITY_INSTANCE, dashboardModel?.tei?.uid()) + dashboardModel?.currentOrgUnit?.uid() + ?.takeIf { enrollmentOrgUnitInCaptureScope(it) }?.let { + bundle.putString(Constants.ORG_UNIT, it) + } + + bundle.putString(Constants.ENROLLMENT_UID, dashboardModel?.currentEnrollment?.uid()) + bundle.putString(Constants.EVENT_CREATION_TYPE, eventCreationType.name) + bundle.putInt(Constants.EVENT_SCHEDULE_INTERVAL, scheduleIntervalDays) + val intent = Intent(view.context, ProgramStageSelectionActivity::class.java) + intent.putExtras(bundle) + contractHandler.createEvent(intent).observe(view.viewLifecycleOwner()) { + dashboardActivityPresenter.init() + } + } + fun onScheduleSelected(uid: String?, sharedView: View?) { uid?.let { val intent = getIntent(view.context, uid) @@ -431,7 +492,9 @@ class TEIDataPresenter( Preference.GROUPING, groups, ) + _groupEvents.value = shouldGroupBool groupingProcessor.onNext(shouldGroupBool) + updateCreateEventButtonVisibility(shouldGroupBool) } } @@ -481,12 +544,7 @@ class TEIDataPresenter( } private fun canAddNewEvents(): Boolean { - return d2.enrollmentModule() - .enrollmentService() - .blockingGetAllowEventCreation( - enrollmentUid, - stagesToHide, - ) + return d2.canCreateEventInEnrollment(enrollmentUid, stagesToHide) } fun getOrgUnitName(orgUnitUid: String): String { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt new file mode 100644 index 0000000000..8577a0dd37 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataContractHandler.kt @@ -0,0 +1,56 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata + +import android.content.Intent +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import javax.inject.Inject + +class TeiDataContractHandler @Inject constructor( + registry: ActivityResultRegistry, +) { + + private val contractNewEventResult = MutableLiveData() + private val contractEditEvent = MutableLiveData() + private val contractScheduleEvent = MutableLiveData() + + private val createEvent = + registry.register(REGISTRY_NEW_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractNewEventResult.value = Unit + } + private val editEvent = + registry.register(REGISTRY_EDIT_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractEditEvent.value = Unit + } + + private val scheduleEvent = + registry.register(REGISTRY_SCHEDULE_EVENT, ActivityResultContracts.StartActivityForResult()) { + contractScheduleEvent.value = Unit + } + + fun createEvent(intent: Intent): LiveData { + createEvent.launch(intent) + return contractNewEventResult + } + + fun editEvent(intent: Intent): LiveData { + editEvent.launch(intent) + return contractEditEvent + } + + fun scheduleEvent( + intent: Intent, + options: ActivityOptionsCompat = ActivityOptionsCompat.makeBasic(), + ): LiveData { + scheduleEvent.launch(intent, options) + return contractScheduleEvent + } + + companion object { + private const val REGISTRY_NEW_EVENT = "New Event" + private const val REGISTRY_EDIT_EVENT = "Edit Event" + private const val REGISTRY_SCHEDULE_EVENT = "Schedule Event" + } +} diff --git a/app/src/main/res/layout-land/fragment_tei_data.xml b/app/src/main/res/layout-land/fragment_tei_data.xml index ba827db9ad..6803452894 100644 --- a/app/src/main/res/layout-land/fragment_tei_data.xml +++ b/app/src/main/res/layout-land/fragment_tei_data.xml @@ -33,14 +33,6 @@ name="followup" type="androidx.databinding.ObservableBoolean" /> - - - - diff --git a/app/src/main/res/layout/fragment_tei_data.xml b/app/src/main/res/layout/fragment_tei_data.xml index bd77458dab..e30b7e12ed 100644 --- a/app/src/main/res/layout/fragment_tei_data.xml +++ b/app/src/main/res/layout/fragment_tei_data.xml @@ -32,15 +32,6 @@ - - - - - + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintVertical_weight="0" + tools:listitem="@layout/item_event" /> - + diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt index a0e3d67478..968de083d0 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt @@ -1,6 +1,13 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.data +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.MutableLiveData import io.reactivex.Single +import org.dhis2.commons.bindings.canCreateEventInEnrollment +import org.dhis2.commons.bindings.enrollment import org.dhis2.commons.data.EventViewModel import org.dhis2.commons.data.EventViewModelType import org.dhis2.commons.filters.FilterManager @@ -12,17 +19,22 @@ import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.usescases.teiDashboard.DashboardRepository +import org.dhis2.usescases.teiDashboard.TeiDashboardPresenter import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.EventCreationOptionsMapper import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataContracts import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataPresenter +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataContractHandler import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataRepository import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions import org.dhis2.utils.analytics.AnalyticsHelper import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mockito import org.mockito.kotlin.any @@ -34,6 +46,10 @@ import java.util.Date class TeiDataPresenterTest { + @Rule + @JvmField + var instantExecutorRule = InstantTaskExecutorRule() + private val view: TEIDataContracts.View = mock() private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) private val dashboardRepository: DashboardRepository = mock() @@ -49,10 +65,12 @@ class TeiDataPresenterTest { private val filterRepository: FilterRepository = mock() private lateinit var teiDataPresenter: TEIDataPresenter private val valueStore: FormValueStore = mock() - private val resources: ResourceManager = mock() private val optionsRepository: OptionsRepository = mock() private val getNewEventCreationTypeOptions: GetNewEventCreationTypeOptions = mock() - private val eventCreationOptionsMapper: EventCreationOptionsMapper = mock() + private val resources: ResourceManager = mock() + private val eventCreationOptionsMapper = EventCreationOptionsMapper(resources) + private val teiDataContractHandler: TeiDataContractHandler = mock() + private val activityPresenter: TeiDashboardPresenter = mock() @Before fun setUp() { @@ -71,10 +89,11 @@ class TeiDataPresenterTest { filterManager, filterRepository, valueStore, - resources, optionsRepository, getNewEventCreationTypeOptions, eventCreationOptionsMapper, + teiDataContractHandler, + activityPresenter, ) } @@ -143,7 +162,47 @@ class TeiDataPresenterTest { assert(stage.applyHideStage(true) == stage) } - private fun fakeModel(eventCount: Int = 0, type: EventViewModelType = EventViewModelType.STAGE): EventViewModel { + @Test + fun shouldSuccessfullyCreateANewEvent() { + val lifecycleOwner: LifecycleOwner = Mockito.mock(LifecycleOwner::class.java) + val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java)) + lifecycle.currentState = Lifecycle.State.RESUMED + Mockito.`when`(lifecycleOwner.lifecycle).thenReturn(lifecycle) + + val contractLiveData = MutableLiveData() + whenever(view.viewLifecycleOwner()) doReturn lifecycleOwner + whenever(teiDataContractHandler.createEvent(any())) doReturn contractLiveData + teiDataPresenter.onEventCreationClick(EventCreationOptionsMapper.ADD_NEW_ID) + contractLiveData.value = Unit + verify(activityPresenter).init() + } + + @Test + fun shouldNotBeAbleToCreateNewEventsWhenFull() { + val mockedEnrollment = mock { + on { status() } doReturn EnrollmentStatus.ACTIVE + } + whenever(d2.enrollment(enrollmentUid)) doReturn mockedEnrollment + whenever(d2.canCreateEventInEnrollment(enrollmentUid, emptyList())) doReturn false + teiDataPresenter.updateCreateEventButtonVisibility(false) + assertTrue(teiDataPresenter.shouldDisplayEventCreationButton.value == false) + } + + @Test + fun shouldNotBeAbleToCreateNewEventsWhenEnrollmentNotActive() { + val mockedEnrollment = mock { + on { status() } doReturn EnrollmentStatus.CANCELLED + } + whenever(d2.enrollment(enrollmentUid)) doReturn mockedEnrollment + whenever(d2.canCreateEventInEnrollment(enrollmentUid, emptyList())) doReturn true + teiDataPresenter.updateCreateEventButtonVisibility(false) + assertTrue(teiDataPresenter.shouldDisplayEventCreationButton.value == false) + } + + private fun fakeModel( + eventCount: Int = 0, + type: EventViewModelType = EventViewModelType.STAGE, + ): EventViewModel { val dataElements = mutableListOf>() dataElements.add( Pair("Name", "Peter"), diff --git a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt index 1c5ff1a888..a000609fd8 100644 --- a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt +++ b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt @@ -4,10 +4,13 @@ import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.work.WorkInfo import io.reactivex.Completable import io.reactivex.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.dhis2.commons.Constants import org.dhis2.commons.sync.ConflictType import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.viewmodel.DispatcherProvider @@ -15,11 +18,16 @@ import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.data.service.workManager.WorkManagerController import org.dhis2.usescases.sms.SmsSendingService import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.category.CategoryOptionCombo +import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.dataset.DataSet +import org.hisp.dhis.android.core.dataset.DataSetElement import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyList import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -283,4 +291,176 @@ class GranularSyncPresenterTest { verify(repository).getUiState() } + + @Test + fun shouldSyncProgram() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.TrackerProgram("programUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncTei() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.TrackerProgramTei("enrollmentUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncEvent() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Event("eventUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncDataSet() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.DataSet("dataSetUid"), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldSyncDataValues() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.DataSetInstance( + "dataSetUid", + "periodId", + "orgUnitUid", + "attrOptionComboUid", + ), + workManager, + smsSyncProvider, + ) + + val mockedDataSet: DataSet = mock { + on { dataSetElements() } doReturn listOf( + DataSetElement.builder() + .categoryCombo(ObjectWithUid.create("catComboUid")) + .dataElement(ObjectWithUid.create("dataElementUid")) + .dataSet(ObjectWithUid.create("dataSetUid")) + .build(), + ) + } + whenever( + d2.dataSetModule().dataSets().withDataSetElements().uid(any()).get(), + ) doReturn Single.just(mockedDataSet) + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid(), + ) doReturn mock() + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid().`in`(anyList()), + ) doReturn mock() + whenever( + d2.categoryModule().categoryOptionCombos().byCategoryComboUid().`in`(anyList()).get(), + ) doReturn Single.just( + listOf(CategoryOptionCombo.builder().uid("catComboUid").build()), + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).beginUniqueWork(any()) + verify(workInfoObserver).onChanged(anyList()) + } + + @Test + fun shouldPerformInitialSync() { + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Global(), + workManager, + smsSyncProvider, + ) + + val workInfoList = MutableLiveData>(emptyList()) + whenever(workManager.getWorkInfosForUniqueWorkLiveData(any())) doReturn workInfoList + + val workInfoObserver: Observer> = mock() + val resultLiveData = presenter.initGranularSync() + resultLiveData.observeForever(workInfoObserver) + + verify(workManager).syncDataForWorker(Constants.DATA_NOW, Constants.INITIAL_SYNC) + verify(workInfoObserver).onChanged(anyList()) + } } diff --git a/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt index 798de30b5b..6a34aef226 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/SdkExtensions.kt @@ -116,6 +116,13 @@ fun D2.enrollmentImportConflicts(enrollmentUid: String): List): Boolean = + enrollmentModule().enrollmentService() + .blockingGetAllowEventCreation( + enrollmentUid, + stagesToHide, + ) + fun D2.teiAttribute(attributeUid: String) = trackedEntityModule().trackedEntityAttributes() .uid(attributeUid).blockingGet()