diff --git a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt index 955969e86b..71dcd45b2f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -23,8 +24,13 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EMPTY_CATEGORY_SELECTOR import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.INPUT_EVENT_INITIAL_DATE +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsViewModel import org.hisp.dhis.android.core.category.CategoryCombo @@ -55,7 +61,7 @@ class EventInitialTest { val date: Date? = dateFormat.parse(dateString) private val eventDetailsRepository: EventDetailsRepository = mock { - on { getProgramStage()} doReturn programStage + on { getProgramStage() } doReturn programStage on { catCombo() } doReturn catCombo on { getEvent() } doReturn null on { getObjectStyle() } doReturn style @@ -64,7 +70,8 @@ class EventInitialTest { on { getCatOptionCombos(CAT_COMBO_UID) } doReturn listOf(categoryOptionCombo) on { getEditableStatus() } doReturn EventEditableStatus.Editable() on { getEnrollmentDate(ENROLLMENT_UID) } doReturn date - on { getStageLastDate(ENROLLMENT_UID) } doReturn DateUtils.uiDateFormat().parse("20/8/2023")!! + on { getStageLastDate(ENROLLMENT_UID) } doReturn DateUtils.uiDateFormat() + .parse("20/8/2023")!! } @@ -99,9 +106,9 @@ class EventInitialTest { on { value } doReturn COORDINATES } - val eventDetailResourcesProvider: EventDetailResourcesProvider = mock { - on { provideDueDate() } doReturn "Due date" - } + private val eventDetailResourcesProvider: EventDetailResourcesProvider = mock { + on { provideDueDate() } doReturn "Due date" + } private fun createConfigureEventTemp(eventCreationType: EventCreationType) = ConfigureEventTemp( creationType = eventCreationType, @@ -213,14 +220,16 @@ class EventInitialTest { val date by viewModel.eventDate.collectAsState() val details by viewModel.eventDetails.collectAsState() ProvideInputDate( - eventDate = date, - detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> - viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) - }, - onClear = { viewModel.onClearEventReportDate() }, - required = true, + EventInputDateUiModel( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = { viewModel.onDateClick() }, + onDateSet = { dateValues -> + viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) + }, + onClear = { viewModel.onClearEventReportDate() }, + required = true, + ) ) } @@ -244,14 +253,17 @@ class EventInitialTest { val date by viewModel.eventDate.collectAsState() val details by viewModel.eventDetails.collectAsState() ProvideInputDate( - eventDate = date, - detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> - viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) - }, - onClear = { viewModel.onClearEventReportDate() }, - required = true, + EventInputDateUiModel( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = { viewModel.onDateClick() }, + onDateSet = { dateValues -> + viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) + }, + onClear = { viewModel.onClearEventReportDate() }, + required = true, + ) + ) } @@ -259,4 +271,40 @@ class EventInitialTest { assert(viewModel.eventDate.value.dateValue == "20/8/2023") } + @Test + fun shouldShowEmptyCategorySelectorIfCategoryHasNoOptions() { + + viewModel = initViewModel( + periodType = null, + eventCreationType = EventCreationType.SCHEDULE, + enrollmentStatus = EnrollmentStatus.ACTIVE, + scheduleInterval = 0 + ) + composeTestRule.setContent { + val date by viewModel.eventDate.collectAsState() + val details by viewModel.eventDetails.collectAsState() + val catCombo by viewModel.eventCatCombo.collectAsState() + + ProvideCategorySelector( + modifier = Modifier, + eventCatComboUiModel = EventCatComboUiModel( + EventCategory("UID", "NO OPTIONS ", 0, emptyList()), + eventCatCombo = catCombo, + detailsEnabled = details.enabled, + currentDate = date.currentDate, + selectedOrgUnit = details.selectedOrgUnit, + onShowCategoryDialog = { + }, + onClearCatCombo = { + }, + onOptionSelected = { + }, + required = true, + noOptionsText = "No options available", + catComboText = "No options catCombo", + ) + ) + } + composeTestRule.onNodeWithTag(EMPTY_CATEGORY_SELECTOR).assertIsDisplayed() + } } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt index 1a68faac56..de8bc64028 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt @@ -6,11 +6,7 @@ import android.os.Bundle import android.view.View import android.widget.DatePicker import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.unit.dp import androidx.databinding.DataBindingUtil import org.dhis2.App import org.dhis2.R @@ -20,6 +16,7 @@ import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener import org.dhis2.databinding.ActivityEventScheduledBinding import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.general.ActivityGlobalAbstract @@ -111,28 +108,31 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. ?: getString(R.string.report_date), dateValue = "", ) - Spacer(modifier = Modifier.height(16.dp)) ProvideInputDate( - eventDate = eventDate, - allowsManualInput = false, - detailsEnabled = true, - onDateClick = { setEvenDateListener(programStage.periodType()) }, - onDateSet = {}, - onClear = {}, + EventInputDateUiModel( + eventDate = eventDate, + allowsManualInput = false, + detailsEnabled = true, + onDateClick = { setEvenDateListener(programStage.periodType()) }, + onDateSet = {}, + onClear = {}, + ), + ) if (programStage.hideDueDate() == false) { val dueDate = EventDate( label = programStage.dueDateLabel() ?: getString(R.string.due_date), dateValue = DateUtils.uiDateFormat().format(event.dueDate() ?: ""), ) - Spacer(modifier = Modifier.height(16.dp)) ProvideInputDate( - eventDate = dueDate, - detailsEnabled = true, - onDateClick = { setDueDateListener(programStage.periodType()) }, - onDateSet = {}, - onClear = {}, + EventInputDateUiModel( + eventDate = dueDate, + detailsEnabled = true, + onDateClick = { setDueDateListener(programStage.periodType()) }, + onDateSet = {}, + onClear = {}, + ), ) } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt index 1a1f357032..0029f6ac3d 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/data/EventDetailsRepository.kt @@ -202,6 +202,11 @@ class EventDetailsRepository( .one().blockingGet()?.uid() } + fun getCatOptionComboDisplayName(categoryComboUid: String): String? { + return d2.categoryModule().categoryCombos().uid(categoryComboUid) + .blockingGet()?.displayName() + } + fun getCatOption(selectedOption: String?): CategoryOption? { return d2.categoryModule().categoryOptions().uid(selectedOption).blockingGet() } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt index c766a1ca2e..093f8a971b 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventCatCombo.kt @@ -19,12 +19,13 @@ class ConfigureEventCatCombo( repository.catCombo().apply { val categories = getCategories(this?.categories()) val categoryOptions = getCategoryOptions() - + val catComboUid = getCatComboUid(this?.uid() ?: "", this?.isDefault ?: false) + val catComboDisplayName = getCatComboDisplayName(this?.uid() ?: "") updateSelectedOptions(categoryOption, categories, categoryOptions) return flowOf( EventCatCombo( - uid = getCatComboUid(this?.uid() ?: "", this?.isDefault ?: false), + uid = catComboUid, isDefault = this?.isDefault ?: false, categories = categories, categoryOptions = categoryOptions, @@ -34,6 +35,7 @@ class ConfigureEventCatCombo( categories = this?.categories(), selectedCategoryOptions = selectedCategoryOptions, ), + displayName = catComboDisplayName, ), ) } @@ -73,6 +75,10 @@ class ConfigureEventCatCombo( return null } + private fun getCatComboDisplayName(categoryComboUid: String): String? { + return repository.getCatOptionComboDisplayName(categoryComboUid) + } + private fun updateSelectedOptions( categoryOption: Pair?, categories: List, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatCombo.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatCombo.kt index cf9553940b..3f17686542 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatCombo.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatCombo.kt @@ -9,4 +9,5 @@ data class EventCatCombo( val categoryOptions: Map? = null, val selectedCategoryOptions: Map = HashMap(), val isCompleted: Boolean = false, + val displayName: String? = "", ) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt new file mode 100644 index 0000000000..2554c5f580 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventCatComboUiModel.kt @@ -0,0 +1,19 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models + +import org.hisp.dhis.android.core.category.CategoryOption +import java.util.Date + +data class EventCatComboUiModel( + val category: EventCategory, + val eventCatCombo: EventCatCombo, + val detailsEnabled: Boolean, + val currentDate: Date?, + val selectedOrgUnit: String?, + val onShowCategoryDialog: (EventCategory) -> Unit, + val onClearCatCombo: (EventCategory) -> Unit, + val onOptionSelected: (CategoryOption?) -> Unit, + val required: Boolean = false, + val noOptionsText: String, + val catComboText: String, + val showField: Boolean = true, +) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt new file mode 100644 index 0000000000..c7f92b621c --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventInputDateUiModel.kt @@ -0,0 +1,14 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models + +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues + +data class EventInputDateUiModel( + val eventDate: EventDate, + val detailsEnabled: Boolean, + val onDateClick: () -> Unit, + val allowsManualInput: Boolean = true, + val onDateSet: (InputDateValues) -> Unit, + val onClear: () -> Unit, + val required: Boolean = false, + val showField: Boolean = true, +) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index 8b159560df..69c297b4a4 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -1,6 +1,8 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi @@ -14,23 +16,22 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp import org.dhis2.R import org.dhis2.commons.resources.ResourceManager import org.dhis2.data.dhislogic.inDateRange import org.dhis2.data.dhislogic.inOrgUnit import org.dhis2.form.model.UiEventType import org.dhis2.form.model.UiRenderType -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus import org.dhis2.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.arch.helpers.Result -import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.common.ValueType @@ -51,63 +52,48 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException -import java.util.Date @Composable fun ProvideInputDate( - eventDate: EventDate, - detailsEnabled: Boolean, - onDateClick: () -> Unit, - allowsManualInput: Boolean = true, - onDateSet: (InputDateValues) -> Unit, - onClear: () -> Unit, - required: Boolean = false, + uiModel: EventInputDateUiModel, + modifier: Modifier = Modifier, ) { - var value by remember(eventDate.dateValue) { - mutableStateOf(eventDate.dateValue?.let { formatStoredDateToUI(it) }) - } + if (uiModel.showField) { + Spacer(modifier = Modifier.height(16.dp)) + var value by remember(uiModel.eventDate.dateValue) { + mutableStateOf(uiModel.eventDate.dateValue?.let { formatStoredDateToUI(it) }) + } - var state by remember { - mutableStateOf(getInputState(detailsEnabled)) - } + var state by remember { + mutableStateOf(getInputState(uiModel.detailsEnabled)) + } - InputDateTime( - title = eventDate.label ?: "", - allowsManualInput = allowsManualInput, - value = value, - actionIconType = DateTimeActionIconType.DATE, - onActionClicked = onDateClick, - state = state, - visualTransformation = DateTransformation(), - onValueChanged = { - value = it - if (it.isEmpty()) { - onClear() - } else if (isValid(it)) { - if (isValidDateFormat(it)) { - state = InputShellState.FOCUSED - formatUIDateToStored(it)?.let { dateValues -> - onDateSet(dateValues) - } - } else { - state = InputShellState.ERROR - } - } else { - state = InputShellState.FOCUSED - } - }, - isRequired = required, - modifier = Modifier.testTag(INPUT_EVENT_INITIAL_DATE), - onFocusChanged = { focused -> - if (!focused) { - value?.let { - if (!isValid(it)) { - state = InputShellState.ERROR + InputDateTime( + title = uiModel.eventDate.label ?: "", + allowsManualInput = uiModel.allowsManualInput, + value = value, + actionIconType = DateTimeActionIconType.DATE, + onActionClicked = uiModel.onDateClick, + state = state, + visualTransformation = DateTransformation(), + onValueChanged = { + value = it + state = getInputShellStateBasedOnValue(it) + manageActionBasedOnValue(uiModel, it) + }, + isRequired = uiModel.required, + modifier = modifier.testTag(INPUT_EVENT_INITIAL_DATE), + onFocusChanged = { focused -> + if (!focused) { + value?.let { + if (!isValid(it)) { + state = InputShellState.ERROR + } } } - } - }, - ) + }, + ) + } } fun isValidDateFormat(dateString: String): Boolean { @@ -130,6 +116,26 @@ fun isValidDateFormat(dateString: String): Boolean { } } +fun getInputShellStateBasedOnValue(dateString: String?): InputShellState { + dateString?.let { + return if (isValid(it) && !isValidDateFormat(it)) { + InputShellState.ERROR + } else { + InputShellState.FOCUSED + } + } + return InputShellState.FOCUSED +} + +fun manageActionBasedOnValue(uiModel: EventInputDateUiModel, dateString: String) { + if (dateString.isEmpty()) { + uiModel.onClear() + } else if (isValid(dateString) && isValidDateFormat(dateString)) { + formatUIDateToStored(dateString)?.let { dateValues -> + uiModel.onDateSet(dateValues) + } + } +} private fun isValid(valueString: String) = valueString.length == 8 private fun formatStoredDateToUI(dateValue: String): String? { @@ -175,115 +181,180 @@ fun ProvideOrgUnit( resources: ResourceManager, onClear: () -> Unit, required: Boolean = false, + showField: Boolean = true, ) { - val state = getInputState(detailsEnabled && orgUnit.enable && orgUnit.orgUnits.size > 1) + if (showField) { + Spacer(modifier = Modifier.height(16.dp)) + val state = getInputState(detailsEnabled && orgUnit.enable && orgUnit.orgUnits.size > 1) - var inputFieldValue by remember(orgUnit.selectedOrgUnit) { - mutableStateOf(orgUnit.selectedOrgUnit?.displayName()) - } + var inputFieldValue by remember(orgUnit.selectedOrgUnit) { + mutableStateOf(orgUnit.selectedOrgUnit?.displayName()) + } - InputOrgUnit( - title = resources.getString(R.string.org_unit), - state = state, - inputText = inputFieldValue ?: "", - onValueChanged = { - inputFieldValue = it - if (it.isNullOrEmpty()) { - onClear() - } - }, - onOrgUnitActionCLicked = onOrgUnitClick, - isRequiredField = required, - ) + InputOrgUnit( + title = resources.getString(R.string.org_unit), + state = state, + inputText = inputFieldValue ?: "", + onValueChanged = { + inputFieldValue = it + if (it.isNullOrEmpty()) { + onClear() + } + }, + onOrgUnitActionCLicked = onOrgUnitClick, + isRequiredField = required, + ) + } } @OptIn(ExperimentalMaterialApi::class) @Composable fun ProvideCategorySelector( modifier: Modifier = Modifier, - category: EventCategory, - eventCatCombo: EventCatCombo, - detailsEnabled: Boolean, - currentDate: Date?, - selectedOrgUnit: String?, - onShowCategoryDialog: (EventCategory) -> Unit, - onClearCatCombo: (EventCategory) -> Unit, - onOptionSelected: (CategoryOption?) -> Unit, - required: Boolean = false, + eventCatComboUiModel: EventCatComboUiModel, ) { var selectedItem by remember { mutableStateOf( - eventCatCombo.selectedCategoryOptions[category.uid]?.displayName() - ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName(), + eventCatComboUiModel.eventCatCombo.selectedCategoryOptions[eventCatComboUiModel.category.uid]?.displayName() + ?: eventCatComboUiModel.eventCatCombo.categoryOptions?.get(eventCatComboUiModel.category.uid)?.displayName(), ) } var expanded by remember { mutableStateOf(false) } + val selectableOptions = eventCatComboUiModel.category.options + .filter { option -> + option.access().data().write() + }.filter { option -> + option.inDateRange(eventCatComboUiModel.currentDate) + }.filter { option -> + option.inOrgUnit(eventCatComboUiModel.selectedOrgUnit) + } + + Spacer(modifier = Modifier.height(16.dp)) + if (selectableOptions.isNotEmpty()) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = {}, + ) { + InputDropDown( + modifier = modifier.testTag(CATEGORY_SELECTOR), + title = eventCatComboUiModel.category.name, + state = getInputState(eventCatComboUiModel.detailsEnabled), + selectedItem = selectedItem, + onResetButtonClicked = { + selectedItem = null + eventCatComboUiModel.onClearCatCombo(eventCatComboUiModel.category) + }, + onArrowDropDownButtonClicked = { + expanded = !expanded + }, + isRequiredField = eventCatComboUiModel.required, + ) + + if (expanded) { + if (eventCatComboUiModel.category.optionsSize > DEFAULT_COUNT_LIMIT) { + eventCatComboUiModel.onShowCategoryDialog(eventCatComboUiModel.category) + expanded = false + } else { + DropdownMenu( + modifier = modifier.exposedDropdownSize(), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + if (selectableOptions.isNotEmpty()) { + selectableOptions.forEach { option -> + val isSelected = option.displayName() == selectedItem + DropdownMenuItem( + modifier = Modifier.background( + when { + isSelected -> SurfaceColor.PrimaryContainer + else -> Color.Transparent + }, + ), + content = { + Text( + text = option.displayName() ?: option.code() ?: "", + color = when { + isSelected -> TextColor.OnPrimaryContainer + else -> TextColor.OnSurface + }, + ) + }, + onClick = { + expanded = false + selectedItem = option.displayName() + eventCatComboUiModel.onOptionSelected(option) + }, + ) + } + } + } + } + } + } + } else { + ProvideEmptyCategorySelector(modifier = modifier, name = eventCatComboUiModel.category.name, option = eventCatComboUiModel.noOptionsText) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ProvideEmptyCategorySelector( + modifier: Modifier = Modifier, + name: String, + option: String, +) { + var selectedItem by remember { + mutableStateOf("") + } + var expanded by remember { mutableStateOf(false) } + Spacer(modifier = Modifier.height(16.dp)) ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = {}, ) { InputDropDown( - modifier = modifier, - title = category.name, - state = getInputState(detailsEnabled), + modifier = modifier.testTag(EMPTY_CATEGORY_SELECTOR), + title = name, + state = InputShellState.UNFOCUSED, selectedItem = selectedItem, onResetButtonClicked = { - selectedItem = null - onClearCatCombo(category) + selectedItem = "" }, onArrowDropDownButtonClicked = { expanded = !expanded }, - isRequiredField = required, + isRequiredField = true, ) - if (expanded) { - if (category.optionsSize > DEFAULT_COUNT_LIMIT) { - onShowCategoryDialog(category) - expanded = false - } else { - DropdownMenu( - modifier = modifier.exposedDropdownSize(), - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - val selectableOptions = category.options - .filter { option -> - option.access().data().write() - }.filter { option -> - option.inDateRange(currentDate) - }.filter { option -> - option.inOrgUnit(selectedOrgUnit) - } - selectableOptions.forEach { option -> - val isSelected = option.displayName() == selectedItem - DropdownMenuItem( - modifier = Modifier.background( - when { - isSelected -> SurfaceColor.PrimaryContainer - else -> Color.Transparent - }, - ), - content = { - Text( - text = option.displayName() ?: option.code() ?: "", - color = when { - isSelected -> TextColor.OnPrimaryContainer - else -> TextColor.OnSurface - }, - ) - }, - onClick = { - expanded = false - selectedItem = option.displayName() - onOptionSelected(option) - }, - ) - } - } - } + DropdownMenu( + modifier = modifier.exposedDropdownSize(), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + val isSelected = option == selectedItem + DropdownMenuItem( + modifier = Modifier.background( + when { + isSelected -> SurfaceColor.PrimaryContainer + else -> Color.Transparent + }, + ), + content = { + Text( + text = option, + color = when { + isSelected -> TextColor.OnPrimaryContainer + else -> TextColor.OnSurface + }, + ) + }, + onClick = { + expanded = false + selectedItem = option + }, + ) } } } @@ -299,35 +370,39 @@ fun ProvideCoordinates( coordinates: EventCoordinates, detailsEnabled: Boolean, resources: ResourceManager, + showField: Boolean = true, ) { - when (coordinates.model?.renderingType) { - UiRenderType.POLYGON, UiRenderType.MULTI_POLYGON -> { - InputPolygon( - title = resources.getString(R.string.polygon), - state = getInputState(detailsEnabled && coordinates.model.editable), - polygonAdded = !coordinates.model.value.isNullOrEmpty(), - onResetButtonClicked = { coordinates.model.onClear() }, - onUpdateButtonClicked = { - coordinates.model.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) - }, - ) - } + if (showField) { + Spacer(modifier = Modifier.height(16.dp)) + when (coordinates.model?.renderingType) { + UiRenderType.POLYGON, UiRenderType.MULTI_POLYGON -> { + InputPolygon( + title = resources.getString(R.string.polygon), + state = getInputState(detailsEnabled && coordinates.model.editable), + polygonAdded = !coordinates.model.value.isNullOrEmpty(), + onResetButtonClicked = { coordinates.model.onClear() }, + onUpdateButtonClicked = { + coordinates.model.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) + }, + ) + } - else -> { - InputCoordinate( - title = resources.getString(R.string.coordinates), - state = getInputState(detailsEnabled && coordinates.model?.editable == true), - coordinates = mapGeometry(coordinates.model?.value, FeatureType.POINT), - latitudeText = resources.getString(R.string.latitude), - longitudeText = resources.getString(R.string.longitude), - addLocationBtnText = resources.getString(R.string.add_location), - onResetButtonClicked = { - coordinates.model?.onClear() - }, - onUpdateButtonClicked = { - coordinates.model?.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) - }, - ) + else -> { + InputCoordinate( + title = resources.getString(R.string.coordinates), + state = getInputState(detailsEnabled && coordinates.model?.editable == true), + coordinates = mapGeometry(coordinates.model?.value, FeatureType.POINT), + latitudeText = resources.getString(R.string.latitude), + longitudeText = resources.getString(R.string.longitude), + addLocationBtnText = resources.getString(R.string.add_location), + onResetButtonClicked = { + coordinates.model?.onClear() + }, + onUpdateButtonClicked = { + coordinates.model?.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) + }, + ) + } } } } @@ -352,44 +427,50 @@ fun ProvideRadioButtons( detailsEnabled: Boolean, resources: ResourceManager, onEventTempSelected: (status: EventTempStatus?) -> Unit, + showField: Boolean = true, ) { - val radioButtonData = listOf( - RadioButtonData( - uid = EventTempStatus.ONE_TIME.name, - selected = eventTemp.status == EventTempStatus.ONE_TIME, - enabled = true, - textInput = resources.getString(R.string.one_time), - ), - RadioButtonData( - uid = EventTempStatus.PERMANENT.name, - selected = eventTemp.status == EventTempStatus.PERMANENT, - enabled = true, - textInput = resources.getString(R.string.permanent), - ), - ) - - InputRadioButton( - title = resources.getString(R.string.referral), - radioButtonData = radioButtonData, - orientation = Orientation.HORIZONTAL, - state = getInputState(detailsEnabled), - itemSelected = radioButtonData.find { it.selected }, - onItemChange = { data -> - when (data?.uid) { - EventTempStatus.ONE_TIME.name -> { - onEventTempSelected(EventTempStatus.ONE_TIME) - } + if (showField) { + Spacer(modifier = Modifier.height(16.dp)) + val radioButtonData = listOf( + RadioButtonData( + uid = EventTempStatus.ONE_TIME.name, + selected = eventTemp.status == EventTempStatus.ONE_TIME, + enabled = true, + textInput = resources.getString(R.string.one_time), + ), + RadioButtonData( + uid = EventTempStatus.PERMANENT.name, + selected = eventTemp.status == EventTempStatus.PERMANENT, + enabled = true, + textInput = resources.getString(R.string.permanent), + ), + ) - EventTempStatus.PERMANENT.name -> { - onEventTempSelected(EventTempStatus.PERMANENT) - } + InputRadioButton( + title = resources.getString(R.string.referral), + radioButtonData = radioButtonData, + orientation = Orientation.HORIZONTAL, + state = getInputState(detailsEnabled), + itemSelected = radioButtonData.find { it.selected }, + onItemChange = { data -> + when (data?.uid) { + EventTempStatus.ONE_TIME.name -> { + onEventTempSelected(EventTempStatus.ONE_TIME) + } + + EventTempStatus.PERMANENT.name -> { + onEventTempSelected(EventTempStatus.PERMANENT) + } - else -> { - onEventTempSelected(null) + else -> { + onEventTempSelected(null) + } } - } - }, - ) + }, + ) + } } const val INPUT_EVENT_INITIAL_DATE = "INPUT_EVENT_INITIAL_DATE" +const val EMPTY_CATEGORY_SELECTOR = "EMPTY_CATEGORY_SELECTOR" +const val CATEGORY_SELECTOR = "CATEGORY_SELECTOR" diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt index 74561abba0..4a3f291399 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt @@ -9,12 +9,9 @@ import android.view.ViewGroup import android.widget.DatePicker import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -39,10 +36,18 @@ import org.dhis2.databinding.EventDetailsFragmentBinding import org.dhis2.maps.views.MapSelectorActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCoordinates +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideEmptyCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideRadioButtons @@ -142,80 +147,14 @@ class EventDetailsFragment : FragmentGlobalAbstract() { val coordinates by viewModel.eventCoordinates.collectAsState() val eventTemp by viewModel.eventTemp.collectAsState() - Column { - if (date.active) { - Spacer(modifier = Modifier.height(16.dp)) - ProvideInputDate( - eventDate = date, - detailsEnabled = details.enabled, - onDateClick = { viewModel.onDateClick() }, - onDateSet = { dateValues -> - viewModel.onDateSet(dateValues.year, dateValues.month - 1, dateValues.day) - }, - onClear = { viewModel.onClearEventReportDate() }, - required = true, - ) - } - if (orgUnit.visible) { - Spacer(modifier = Modifier.height(16.dp)) - ProvideOrgUnit( - orgUnit = orgUnit, - detailsEnabled = details.enabled, - onOrgUnitClick = { viewModel.onOrgUnitClick() }, - resources = resourceManager, - onClear = { - viewModel.onClearOrgUnit() - }, - required = true, - ) - } - - if (!catCombo.isDefault) { - catCombo.categories.forEach { category -> - Spacer(modifier = Modifier.height(16.dp)) - ProvideCategorySelector( - category = category, - eventCatCombo = catCombo, - detailsEnabled = details.enabled, - currentDate = date.currentDate, - selectedOrgUnit = details.selectedOrgUnit, - onShowCategoryDialog = { - showCategoryDialog(it) - }, - onClearCatCombo = { - viewModel.onClearCatCombo() - }, - onOptionSelected = { - val selectedOption = Pair(category.uid, it?.uid()) - viewModel.setUpCategoryCombo(selectedOption) - }, - - required = true, - ) - } - } - - if (coordinates.active) { - Spacer(modifier = Modifier.height(16.dp)) - ProvideCoordinates( - coordinates = coordinates, - detailsEnabled = details.enabled, - resources = resourceManager, - ) - } - - if (eventTemp.active) { - Spacer(modifier = Modifier.height(16.dp)) - ProvideRadioButtons( - eventTemp = eventTemp, - detailsEnabled = details.enabled, - resources = resourceManager, - onEventTempSelected = { - viewModel.setUpEventTemp(it) - }, - ) - } - } + ProvideNewEventForm( + date = date, + details = details, + orgUnit = orgUnit, + catCombo = catCombo, + coordinates = coordinates, + eventTemp = eventTemp, + ) } return binding.root } @@ -291,6 +230,87 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } } + @Composable + private fun ProvideNewEventForm( + date: EventDate, + details: EventDetails, + orgUnit: EventOrgUnit, + catCombo: EventCatCombo, + coordinates: EventCoordinates, + eventTemp: EventTemp, + ) { + Column { + ProvideInputDate( + EventInputDateUiModel( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = { viewModel.onDateClick() }, + onDateSet = { dateValues -> + viewModel.onDateSet(dateValues.year, dateValues.month - 1, dateValues.day) + }, + onClear = { viewModel.onClearEventReportDate() }, + required = true, + showField = date.active, + ), + ) + ProvideOrgUnit( + orgUnit = orgUnit, + detailsEnabled = details.enabled, + onOrgUnitClick = { viewModel.onOrgUnitClick() }, + resources = resourceManager, + onClear = { + viewModel.onClearOrgUnit() + }, + required = true, + showField = orgUnit.visible, + ) + + if (!catCombo.isDefault && catCombo.categories.isNotEmpty()) { + catCombo.categories.forEach { category -> + ProvideCategorySelector( + eventCatComboUiModel = EventCatComboUiModel( + category = category, + eventCatCombo = catCombo, + detailsEnabled = details.enabled, + currentDate = date.currentDate, + selectedOrgUnit = details.selectedOrgUnit, + onShowCategoryDialog = { + showCategoryDialog(it) + }, + onClearCatCombo = { + viewModel.onClearCatCombo() + }, + onOptionSelected = { + val selectedOption = Pair(category.uid, it?.uid()) + viewModel.setUpCategoryCombo(selectedOption) + }, + required = true, + noOptionsText = getString(R.string.no_options), + catComboText = getString(R.string.cat_combo), + ), + ) + } + } else if (!catCombo.isDefault) { + ProvideEmptyCategorySelector(name = catCombo.displayName ?: getString(R.string.cat_combo), option = getString(R.string.no_options)) + } + ProvideCoordinates( + coordinates = coordinates, + detailsEnabled = details.enabled, + resources = resourceManager, + showField = coordinates.active, + ) + ProvideRadioButtons( + eventTemp = eventTemp, + detailsEnabled = details.enabled, + resources = resourceManager, + onEventTempSelected = { + viewModel.setUpEventTemp(it) + }, + showField = eventTemp.active, + ) + } + } + private fun showCalendarDialog() { val dialog = CalendarPicker(requireContext()) dialog.setInitialDate(viewModel.eventDate.value.currentDate) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt index c157f7e4b8..95272d5297 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt @@ -66,12 +66,7 @@ class ProgramEventDetailPresenter( ), ) compositeDisposable.add( - Single.zip( - Single.just(eventRepository.getAccessDataWrite()), - eventRepository.hasAccessToAllCatOptions(), - ) { hasWritePermission, hasAccessToAllCatOptions -> - hasWritePermission && hasAccessToAllCatOptions - } + Single.just(eventRepository.getAccessDataWrite()) .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) .subscribe( diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt index b7ef01401c..737c8fcbee 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt @@ -17,7 +17,6 @@ interface ProgramEventDetailRepository { fun filteredEventsForMap(): Flowable fun program(): Single fun getAccessDataWrite(): Boolean - fun hasAccessToAllCatOptions(): Single fun getInfoForEvent(eventUid: String): Flowable fun featureType(): Single fun getCatOptCombo(selectedCatOptionCombo: String): CategoryOptionCombo? diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt index 603c3d5fc1..3e349d5ad5 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt @@ -118,34 +118,6 @@ class ProgramEventDetailRepositoryImpl internal constructor( return canWrite } - override fun hasAccessToAllCatOptions(): Single { - return programRepository.get() - .map { program -> - val catCombo = d2.categoryModule().categoryCombos().withCategories() - .uid(program.categoryComboUid()).blockingGet() - var hasAccess = true - if (catCombo?.isDefault == false) { - for (category in catCombo.categories() ?: emptyList()) { - val options = d2.categoryModule().categories().withCategoryOptions() - .uid(category.uid()).blockingGet()?.categoryOptions() ?: emptyList() - var accessibleOptions = options.size - for (categoryOption in options) { - if (d2.categoryModule().categoryOptions().uid(categoryOption.uid()) - .blockingGet()?.access()?.data()?.write() == false - ) { - accessibleOptions-- - } - } - if (accessibleOptions == 0) { - hasAccess = false - break - } - } - } - hasAccess - } - } - override fun workingLists(): Single> { return d2.eventModule().eventFilters() .withEventDataFilters() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 765818b11e..b50c65fc3b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,6 +160,7 @@ Event deleted successfully Total Most recent event date + No options available Enrollment List diff --git a/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt index f3b6a21ba0..b7978a47b3 100644 --- a/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenterTest.kt @@ -117,7 +117,6 @@ class ProgramEventDetailPresenterTest { ) filterManager.sortingItem = SortingItem(Filters.ORG_UNIT, SortingStatus.NONE) whenever(repository.getAccessDataWrite()) doReturn true - whenever(repository.hasAccessToAllCatOptions()) doReturn Single.just(true) whenever(repository.program()) doReturn Single.just(program) whenever( repository.filteredProgramEvents(),