Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [ANDROAPP-5802] schedule events after completion #3483

Merged
merged 8 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading