Skip to content

Commit

Permalink
feat: [ANDROAPP-5802] schedule events after completion (#3483)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmmateos authored Feb 16, 2024
1 parent 5fc8fb9 commit 80b307d
Show file tree
Hide file tree
Showing 17 changed files with 747 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import org.dhis2.usescases.datasets.DataSetTest
import org.dhis2.usescases.enrollment.EnrollmentTest
import org.dhis2.usescases.event.EventTest
import org.dhis2.usescases.filters.FilterTest
import org.dhis2.usescases.form.FormTest
import org.dhis2.usescases.jira.JiraTest
import org.dhis2.usescases.login.LoginTest
import org.dhis2.usescases.main.MainTest
Expand All @@ -15,6 +14,7 @@ import org.dhis2.usescases.searchte.SearchTETest
import org.dhis2.usescases.settings.SettingsTest
import org.dhis2.usescases.sync.SyncActivityTest
import org.dhis2.usescases.teidashboard.TeiDashboardTest
import org.dhis2.usescases.teidashboard.dialogs.scheduling.SchedulingDialogUiTest
import org.junit.runner.RunWith
import org.junit.runners.Suite

Expand All @@ -33,6 +33,7 @@ import org.junit.runners.Suite
SearchTETest::class,
SettingsTest::class,
SyncActivityTest::class,
TeiDashboardTest::class
TeiDashboardTest::class,
SchedulingDialogUiTest::class,
)
class UseCaseTestsSuite
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.dhis2.usescases.teidashboard.dialogs.scheduling

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import kotlinx.coroutines.flow.MutableStateFlow
import org.dhis2.composetable.test.TestActivity
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory
import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialogUi
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingViewModel
import org.hisp.dhis.android.core.category.CategoryOption
import org.hisp.dhis.android.core.program.ProgramStage
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class SchedulingDialogUiTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<TestActivity>()

private val viewModel: SchedulingViewModel = mock()

@Before
fun setUp() {
whenever(viewModel.eventDate).thenReturn(MutableStateFlow(EventDate(label = "Date")))
whenever(viewModel.eventCatCombo).thenReturn(
MutableStateFlow(
EventCatCombo(
categories = listOf(
EventCategory(
uid = "uid",
name = "CatCombo",
optionsSize = 2,
options = listOf(
CategoryOption.builder().uid("uidA").displayName("optionA").build(),
CategoryOption.builder().uid("uidB").displayName("optionB").build(),
),
),
),
),
),
)
}

@Test
fun programStageInputNotDisplayedForOneStage() {
val programStages = listOf(ProgramStage.builder().uid("stageUid").displayName("PS A").build())
whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first()))
composeTestRule.setContent {
SchedulingDialogUi(
programStages = programStages,
viewModel = viewModel,
orgUnitUid = "orgUnitUid",
) {
}
}
composeTestRule.onNodeWithText("Schedule next " + programStages.first().displayName() + "?").assertExists()
composeTestRule.onNodeWithText("Program stage").assertDoesNotExist()
composeTestRule.onNodeWithText("Date").assertExists()
composeTestRule.onNodeWithText("CatCombo *").assertExists()
composeTestRule.onNodeWithText("Schedule").assertExists()
}

@Test
fun programStageInputDisplayedForMoreThanOneStages() {
val programStages = listOf(
ProgramStage.builder().uid("stageUidA").displayName("PS A").build(),
ProgramStage.builder().uid("stageUidB").displayName("PS B").build(),
)
whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first()))
composeTestRule.setContent {
SchedulingDialogUi(
programStages = programStages,
viewModel = viewModel,
orgUnitUid = "orgUnitUid",
) {
}
}
composeTestRule.onNodeWithText("Schedule next event?").assertExists()
composeTestRule.onNodeWithText("Program stage").assertExists()
}

@Test
fun inputFieldsShouldNotBeDisplayedWhenAnsweringNo() {
val programStages = listOf(
ProgramStage.builder().uid("stageUidA").displayName("PS A").build(),
ProgramStage.builder().uid("stageUidB").displayName("PS B").build(),
)
whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first()))
composeTestRule.setContent {
SchedulingDialogUi(
programStages = programStages,
viewModel = viewModel,
orgUnitUid = "orgUnitUid",
) {
}
}
composeTestRule.onNodeWithText("No").performClick()

composeTestRule.onNodeWithText("Program stage").assertDoesNotExist()
composeTestRule.onNodeWithText("Date").assertDoesNotExist()
composeTestRule.onNodeWithText("CatCombo *").assertDoesNotExist()
composeTestRule.onNodeWithText("Done").assertExists()
}

@Test
fun selectProgramStage() {
val programStages = listOf(
ProgramStage.builder().uid("stageUidA").displayName("PS A").build(),
ProgramStage.builder().uid("stageUidB").displayName("PS B").build(),
)
whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first()))
composeTestRule.setContent {
SchedulingDialogUi(
programStages = programStages,
viewModel = viewModel,
orgUnitUid = "orgUnitUid",
) {
}
}

composeTestRule.onNodeWithText("Program stage").performClick()
composeTestRule.onNodeWithTag("INPUT_DROPDOWN_MENU_ITEM_1").performClick()

verify(viewModel).updateStage(programStages[1])
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/org/dhis2/data/user/UserComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
import org.dhis2.usescases.teiDashboard.TeiDashboardModule;
import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipComponent;
import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipModule;
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingComponent;
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingModule;
import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListComponent;
import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListModule;
import org.dhis2.utils.optionset.OptionSetComponent;
Expand Down Expand Up @@ -204,4 +206,7 @@ public interface UserComponent {

@NonNull
SessionComponent plus(PinModule pinModule);

@NonNull
SchedulingComponent plus(SchedulingModule schedulingModule);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data

import io.reactivex.Observable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.dhis2.data.dhislogic.AUTH_ALL
import org.dhis2.data.dhislogic.AUTH_UNCOMPLETE_EVENT
import org.dhis2.form.model.FieldUiModel
Expand All @@ -17,21 +19,23 @@ import org.hisp.dhis.android.core.common.ObjectStyle
import org.hisp.dhis.android.core.common.ValueType
import org.hisp.dhis.android.core.enrollment.EnrollmentStatus
import org.hisp.dhis.android.core.event.Event
import org.hisp.dhis.android.core.event.EventCreateProjection
import org.hisp.dhis.android.core.event.EventEditableStatus
import org.hisp.dhis.android.core.event.EventObjectRepository
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.ProgramStage
import java.util.Calendar
import java.util.Date

class EventDetailsRepository(
private val d2: D2,
private val programUid: String,
private val eventUid: String?,
private val programStageUid: String?,
private val fieldFactory: FieldViewModelFactory,
private val fieldFactory: FieldViewModelFactory?,
private val onError: (Throwable) -> String?,
) {

Expand Down Expand Up @@ -173,7 +177,7 @@ class EventDetailsRepository(
d2.eventModule().events().uid(eventUid).blockingGet()?.geometry()?.coordinates()
}

return fieldFactory.create(
return fieldFactory!!.create(
id = "",
label = "",
valueType = ValueType.COORDINATE,
Expand Down Expand Up @@ -325,4 +329,33 @@ class EventDetailsRepository(
),
)
}

fun scheduleEvent(
enrollmentUid: String?,
dueDate: Date,
orgUnitUid: String?,
categoryOptionComboUid: String?,
): Flow<String?> = flow {
val cal = Calendar.getInstance()
cal.time = dueDate
cal[Calendar.HOUR_OF_DAY] = 0
cal[Calendar.MINUTE] = 0
cal[Calendar.SECOND] = 0
cal[Calendar.MILLISECOND] = 0

val uid = d2.eventModule().events().blockingAdd(
EventCreateProjection.builder()
.enrollment(enrollmentUid)
.program(programUid)
.programStage(programStageUid)
.organisationUnit(orgUnitUid)
.attributeOptionCombo(categoryOptionComboUid)
.build(),
)
val eventRepository = d2.eventModule().events().uid(uid)
eventRepository.setDueDate(cal.time)
eventRepository.setStatus(EventStatus.SCHEDULE)

emit(uid)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class ConfigureEventCatCombo(
repository.catCombo().apply {
val categories = getCategories(this?.categories())
val categoryOptions = getCategoryOptions()
updateSelectedOptions(categoryOption, categories, categoryOptions)
val catComboUid = getCatComboUid(this?.uid() ?: "", this?.isDefault ?: false)
val catComboDisplayName = getCatComboDisplayName(this?.uid() ?: "")
updateSelectedOptions(categoryOption, categories, categoryOptions)

return flowOf(
EventCatCombo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ fun ProvideCategorySelector(
modifier: Modifier = Modifier,
eventCatComboUiModel: EventCatComboUiModel,
) {
var selectedItem by remember {
mutableStateOf(
eventCatComboUiModel.eventCatCombo.selectedCategoryOptions[eventCatComboUiModel.category.uid]?.displayName()
?: eventCatComboUiModel.eventCatCombo.categoryOptions?.get(eventCatComboUiModel.category.uid)?.displayName(),
)
var selectedItem by with(eventCatComboUiModel) {
remember(this) {
mutableStateOf(
eventCatCombo.selectedCategoryOptions[category.uid]?.displayName()
?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName(),
)
}
}

val selectableOptions = eventCatComboUiModel.category.options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class TEIDataContracts {
interface View : AbstractActivityContracts.View {
fun viewLifecycleOwner(): LifecycleOwner
fun setEvents(events: List<EventViewModel>)
fun displayGenerateEvent(): Consumer<ProgramStage>
fun displayScheduleEvent()
fun showDialogCloseProgram()
fun areEventsCompleted(): Consumer<Single<Boolean>>
fun enrollmentCompleted(): Consumer<EnrollmentStatus>
fun switchFollowUp(followUp: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.dhis2.commons.animations.collapse
import org.dhis2.commons.animations.expand
import org.dhis2.commons.data.EventCreationType
import org.dhis2.commons.data.EventViewModel
import org.dhis2.commons.data.EventViewModelType
import org.dhis2.commons.data.StageSection
import org.dhis2.commons.dialogs.CustomDialog
import org.dhis2.commons.dialogs.DialogClickListener
Expand All @@ -53,6 +54,8 @@ import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity
import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.CategoryDialogInteractions
import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventAdapter
import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventCatComboOptionSelector
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog
import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_DIALOG
import org.dhis2.usescases.teiDashboard.ui.TeiDetailDashboard
import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper
import org.dhis2.usescases.teiDashboard.ui.mapper.TeiDashboardCardMapper
Expand Down Expand Up @@ -373,35 +376,26 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View {
}
}

override fun displayGenerateEvent(): Consumer<ProgramStage> {
return Consumer { programStageModel: ProgramStage ->
programStageFromEvent = programStageModel
if (programStageModel.displayGenerateEventBox() == true || programStageModel.allowGenerateNextVisit() == true) {
dialog = CustomDialog(
requireContext(),
getString(R.string.dialog_generate_new_event),
getString(R.string.message_generate_new_event),
getString(R.string.button_ok),
getString(R.string.cancel),
RC_GENERATE_EVENT,
object : DialogClickListener {
override fun onPositive() {
presenter.onAcceptScheduleNewEvent(
programStageModel.standardInterval() ?: 0,
)
}

override fun onNegative() {
if (programStageFromEvent?.remindCompleted() == true) presenter.areEventsCompleted()
}
},
override fun displayScheduleEvent() {
SchedulingDialog(
enrollment = dashboardModel.currentEnrollment,
programStages = eventAdapter?.currentList
?.filter { it.type == EventViewModelType.STAGE && it.canAddNewEvent }
?.mapNotNull { it.stage }
?: emptyList(),
onScheduled = { programStageUid ->
showToast(
resourceManager.formatWithEventLabel(
R.string.event_label_created,
programStageUid,
),
)
dialog?.show()
} else if (java.lang.Boolean.TRUE == programStageModel.remindCompleted()) showDialogCloseProgram()
}
presenter.updateEventList()
},
).show(childFragmentManager, SCHEDULING_DIALOG)
}

private fun showDialogCloseProgram() {
override fun showDialogCloseProgram() {
dialog = CustomDialog(
requireContext(),
resourceManager.formatWithEventLabel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,13 @@ class TEIDataPresenter(
dashboardRepository.displayGenerateEvent(eventUid)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.subscribe(view.displayGenerateEvent(), Timber.Forest::d),
.subscribe({ programStage ->
if (programStage.displayGenerateEventBox() == true || programStage.allowGenerateNextVisit() == true) {
view.displayScheduleEvent()
} else if (programStage.remindCompleted() == true) {
view.showDialogCloseProgram()
}
}, Timber.Forest::d),
)
}

Expand Down Expand Up @@ -579,4 +585,8 @@ class TEIDataPresenter(
fun getTeiHeader(): String? {
return teiDataRepository.getTeiHeader()
}

fun updateEventList() {
groupingProcessor.onNext(_groupEvents.value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.dhis2.usescases.teiDashboard.dialogs.scheduling

import dagger.Subcomponent
import org.dhis2.commons.di.dagger.PerFragment

@PerFragment
@Subcomponent(modules = [SchedulingModule::class])
fun interface SchedulingComponent {
fun inject(schedulingDialog: SchedulingDialog)
}
Loading

0 comments on commit 80b307d

Please sign in to comment.