diff --git a/app/src/main/java/org/dhis2/data/server/ServerModule.kt b/app/src/main/java/org/dhis2/data/server/ServerModule.kt index c9be356f8e..33c532c2e5 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerModule.kt +++ b/app/src/main/java/org/dhis2/data/server/ServerModule.kt @@ -12,6 +12,7 @@ import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.commons.di.dagger.PerServer import org.dhis2.commons.filters.data.GetFiltersApplyingWebAppConfig +import org.dhis2.commons.periods.PeriodUseCase import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ColorUtils @@ -172,6 +173,10 @@ class ServerModule { return ResourceManager(contextWrapper, colorUtils) } + @Provides + @PerServer + fun providePeriodUseCase(d2: D2) = PeriodUseCase(d2) + companion object { @JvmStatic fun getD2Configuration(context: Context): D2Configuration { 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 28952bf12a..163dae4532 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventActivity.kt @@ -8,12 +8,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.databinding.DataBindingUtil +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.App import org.dhis2.R import org.dhis2.commons.date.DateUtils -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.dialogs.AlertBottomDialog +import org.dhis2.commons.periods.PeriodSelectorContent import org.dhis2.databinding.ActivityEventScheduledBinding import org.dhis2.form.model.EventMode +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel @@ -145,7 +149,12 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. uiModel = EventInputDateUiModel( eventDate = eventDate, detailsEnabled = true, - onDateClick = { showEventDatePeriodDialog(programStage.periodType()) }, + onDateClick = { + showPeriodDialog( + periodType = programStage.periodType(), + scheduling = false, + ) + }, onDateSelected = {}, onClear = { }, required = true, @@ -160,7 +169,12 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. uiModel = EventInputDateUiModel( eventDate = dueDate, detailsEnabled = true, - onDateClick = { showDueDatePeriodDialog(programStage.periodType()) }, + onDateClick = { + showPeriodDialog( + periodType = programStage.periodType(), + scheduling = true, + ) + }, onDateSelected = {}, onClear = { }, required = true, @@ -181,58 +195,28 @@ class ScheduledEventActivity : ActivityGlobalAbstract(), ScheduledEventContract. binding.name = program.displayName() } - private fun showEventDatePeriodDialog(periodType: PeriodType?) { - if (periodType != null) { - var minDate = - DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType) - val lastPeriodDate = - DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true) - - if (lastPeriodDate.after( - DateUtils.getInstance().getNextPeriod( - program.expiryPeriodType(), - minDate, - 0, - ), - ) - ) { - minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0) - } - - PeriodDialog() - .setPeriod(periodType) - .setMinDate(minDate) - .setMaxDate(DateUtils.getInstance().today) - .setPossitiveListener { selectedDate -> presenter.setEventDate(selectedDate) } - .show(supportFragmentManager, PeriodDialog::class.java.simpleName) - } - } - - private fun showDueDatePeriodDialog(periodType: PeriodType?) { - if (periodType != null) { - var minDate = - DateUtils.getInstance().expDate(null, program.expiryDays()!!, periodType) - val lastPeriodDate = - DateUtils.getInstance().getNextPeriod(periodType, minDate, -1, true) - - if (lastPeriodDate.after( - DateUtils.getInstance().getNextPeriod( - program.expiryPeriodType(), - minDate, - 0, - ), - ) - ) { - minDate = DateUtils.getInstance().getNextPeriod(periodType, lastPeriodDate, 0) - } - - PeriodDialog() - .setPeriod(periodType) - .setMinDate(minDate) - .setMaxDate(DateUtils.getInstance().today) - .setPossitiveListener { selectedDate -> presenter.setDueDate(selectedDate) } - .show(supportFragmentManager, PeriodDialog::class.java.simpleName) - } + private fun showPeriodDialog(periodType: PeriodType?, scheduling: Boolean) { + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = "PeriodType title", // TODO:6660 + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { bottomSheetDialog -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = presenter.fetchPeriods(scheduling).collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedPeriod -> + presenter.setDueDate(selectedPeriod) + bottomSheetDialog.dismiss() + } + }, + ).show(supportFragmentManager, AlertBottomDialog::class.java.simpleName) } override fun openFormActivity() { diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt index dfc435ff34..797fc2ac6e 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventContract.kt @@ -1,5 +1,8 @@ package org.dhis2.usescases.events +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.Period import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues import org.dhis2.usescases.general.AbstractActivityContracts import org.hisp.dhis.android.core.category.CategoryOption @@ -32,5 +35,6 @@ class ScheduledEventContract { fun getEventTei(): String fun getEnrollment(): Enrollment? fun getSelectableDates(program: Program, isDueDate: Boolean): SelectableDates? + fun fetchPeriods(scheduling: Boolean): Flow> } } diff --git a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt index fc8cebc39c..79d43230d0 100644 --- a/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/events/ScheduledEventPresenterImpl.kt @@ -1,9 +1,18 @@ package org.dhis2.usescases.events +import androidx.paging.PagingData +import androidx.paging.map import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import org.dhis2.commons.bindings.event +import org.dhis2.commons.bindings.programStage import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.periods.Period +import org.dhis2.commons.periods.PeriodUseCase import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MAX_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MIN_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.InputDateValues @@ -12,6 +21,7 @@ import org.hisp.dhis.android.core.arch.helpers.UidsHelper import org.hisp.dhis.android.core.category.CategoryOption import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import timber.log.Timber @@ -27,6 +37,7 @@ class ScheduledEventPresenterImpl( ) : ScheduledEventContract.Presenter { private lateinit var disposable: CompositeDisposable + private val periodUseCase = PeriodUseCase(d2) override fun init() { disposable = CompositeDisposable() @@ -106,11 +117,53 @@ class ScheduledEventPresenterImpl( } else { null } - val minDateString = if (minDate == null) null else SimpleDateFormat("ddMMyyyy", Locale.US).format(minDate) - val maxDateString = if (isDueDate) DEFAULT_MAX_DATE else SimpleDateFormat("ddMMyyyy", Locale.US).format(Date(System.currentTimeMillis() - 1000)) + val minDateString = + if (minDate == null) null else SimpleDateFormat("ddMMyyyy", Locale.US).format(minDate) + val maxDateString = if (isDueDate) { + DEFAULT_MAX_DATE + } else { + SimpleDateFormat( + "ddMMyyyy", + Locale.US, + ).format(Date(System.currentTimeMillis() - 1000)) + } return SelectableDates(minDateString ?: DEFAULT_MIN_DATE, maxDateString) } + override fun fetchPeriods(scheduling: Boolean): Flow> { + return with(periodUseCase) { + val event = d2.event(eventUid) ?: return emptyFlow() + val stage = event.programStage()?.let { d2.programStage(it) } ?: return emptyFlow() + val unavailableDates = getEventUnavailableDates( + programStageUid = stage.uid(), + enrollmentUid = event.enrollment(), + currentEventUid = eventUid, + ) + fetchPeriods( + periodType = stage.periodType() ?: PeriodType.Daily, + selectedDate = if (scheduling) { + event.dueDate() + } else { + event.eventDate() + }, + initialDate = getEventPeriodMinDate( + programStage = stage, + isScheduling = scheduling, + eventEnrollmentUid = event.enrollment(), + ), + maxDate = getEventPeriodMaxDate( + programStage = stage, + isScheduling = scheduling, + eventEnrollmentUid = event.enrollment(), + ), + ).map { paging -> + paging.map { period -> + period.copy(enabled = unavailableDates.contains(period.startDate).not()) + } + } + } + } + override fun setDueDate(date: Date) { d2.eventModule().events().uid(eventUid).setDueDate(date) d2.eventModule().events().uid(eventUid).setStatus(EventStatus.SCHEDULE) 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 a70ef1c12c..652fd0759f 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 @@ -159,6 +159,8 @@ class EventDetailsRepository( else -> OrganisationUnit.Scope.SCOPE_DATA_CAPTURE } + fun isScheduling(): Boolean = eventCreationType == EventCreationType.SCHEDULE + fun getOrganisationUnit(orgUnitUid: String): OrganisationUnit? { return d2.organisationUnitModule().organisationUnits() .byUid() diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt new file mode 100644 index 0000000000..8f3e6f9bd7 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigurePeriodSelector.kt @@ -0,0 +1,51 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain + +import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import org.dhis2.commons.periods.Period +import org.dhis2.commons.periods.PeriodUseCase +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository +import org.hisp.dhis.android.core.period.PeriodType + +class ConfigurePeriodSelector( + private val eventDetailRepository: EventDetailsRepository, + private val periodUseCase: PeriodUseCase, +) { + operator fun invoke(): Flow> { + val programStage = eventDetailRepository.getProgramStage() ?: return emptyFlow() + val event = eventDetailRepository.getEvent() + val periodType = programStage.periodType() ?: PeriodType.Daily + return with(periodUseCase) { + val unavailableDate = getEventUnavailableDates( + programStageUid = programStage.uid(), + enrollmentUid = event?.enrollment(), + currentEventUid = event?.uid(), + ) + fetchPeriods( + periodType = periodType, + selectedDate = if (eventDetailRepository.isScheduling()) { + event?.dueDate() + } else { + event?.eventDate() + }, + initialDate = getEventPeriodMinDate( + programStage = programStage, + isScheduling = eventDetailRepository.isScheduling(), + eventEnrollmentUid = event?.enrollment(), + ), + maxDate = getEventPeriodMaxDate( + programStage = programStage, + isScheduling = eventDetailRepository.isScheduling(), + eventEnrollmentUid = event?.enrollment(), + ), + ).map { paging -> + paging.map { period -> + period.copy(enabled = unavailableDate.contains(period.startDate).not()) + } + } + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt index bb12ddfcd2..efc476c650 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt @@ -6,6 +6,7 @@ import dagger.Provides import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.periods.PeriodUseCase import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.prefs.PreferenceProviderImpl import org.dhis2.commons.resources.DhisPeriodUtils @@ -30,6 +31,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui.EventDetailsViewModelFactory @@ -101,6 +103,18 @@ class EventDetailsModule( ) } + @Provides + @PerFragment + fun provideConfigurePeriodSelector( + eventDetailsRepository: EventDetailsRepository, + periodUseCase: PeriodUseCase, + ): ConfigurePeriodSelector { + return ConfigurePeriodSelector( + eventDetailRepository = eventDetailsRepository, + periodUseCase = periodUseCase, + ) + } + @Provides @PerFragment fun eventDetailsViewModelFactory( @@ -112,6 +126,7 @@ class EventDetailsModule( locationProvider: LocationProvider, eventDetailResourcesProvider: EventDetailResourcesProvider, metadataIconProvider: MetadataIconProvider, + configurePeriodSelector: ConfigurePeriodSelector, ): EventDetailsViewModelFactory { return EventDetailsViewModelFactory( ConfigureEventDetails( @@ -152,6 +167,7 @@ class EventDetailsModule( resourcesProvider = resourcesProvider, ), eventDetailResourcesProvider = eventDetailResourcesProvider, + configurePeriodSelector = configurePeriodSelector, ) } } 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 3475546966..854b499e8a 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 @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.paging.compose.collectAsLazyPagingItems import org.dhis2.R import org.dhis2.commons.Constants.ENROLLMENT_STATUS import org.dhis2.commons.Constants.ENROLLMENT_UID @@ -26,13 +27,16 @@ import org.dhis2.commons.Constants.ORG_UNIT import org.dhis2.commons.Constants.PROGRAM_STAGE_UID import org.dhis2.commons.Constants.PROGRAM_UID import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.PeriodSelectorContent import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.EventDetailsFragmentBinding import org.dhis2.maps.views.MapSelectorActivity +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo @@ -52,7 +56,6 @@ import org.dhis2.usescases.general.FragmentGlobalAbstract import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.period.PeriodType -import java.util.Date import javax.inject.Inject class EventDetailsFragment : FragmentGlobalAbstract() { @@ -314,14 +317,27 @@ class EventDetailsFragment : FragmentGlobalAbstract() { } private fun showPeriodDialog() { - PeriodDialog() - .setPeriod(viewModel.eventDate.value.periodType) - .setMinDate(viewModel.eventDate.value.minDate) - .setMaxDate(viewModel.eventDate.value.maxDate) - .setPossitiveListener { selectedDate: Date -> - viewModel.setUpEventReportDate(selectedDate) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = "PeriodType title", // TODO:6660 + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { bottomSheetDialog -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + viewModel.setUpEventReportDate(selectedDate) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } private fun showOrgUnitDialog() { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index 3c4f1c9e8b..0ec2e53607 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -2,13 +2,16 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import org.dhis2.commons.extensions.truncate import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.periods.Period import org.dhis2.form.data.GeometryController import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo @@ -16,6 +19,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates @@ -49,6 +53,7 @@ class EventDetailsViewModel( private val locationProvider: LocationProvider, private val createOrUpdateEventDetails: CreateOrUpdateEventDetails, private val resourcesProvider: EventDetailResourcesProvider, + private val configurePeriodSelector: ConfigurePeriodSelector, ) : ViewModel() { var showPeriods: (() -> Unit)? = null @@ -348,6 +353,10 @@ class EventDetailsViewModel( fun cancelCoordinateRequest() { setUpCoordinates(value = eventCoordinates.value.model?.value) } + + fun fetchPeriods(): Flow> { + return configurePeriodSelector() + } } inline fun Result.mockSafeFold( diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt index 5910ee7d71..6eba18f613 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt @@ -9,6 +9,7 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigurePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider import org.hisp.dhis.android.core.period.PeriodType @@ -26,6 +27,7 @@ class EventDetailsViewModelFactory( private val locationProvider: LocationProvider, private val createOrUpdateEventDetails: CreateOrUpdateEventDetails, private val eventDetailResourcesProvider: EventDetailResourcesProvider, + private val configurePeriodSelector: ConfigurePeriodSelector, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -41,6 +43,7 @@ class EventDetailsViewModelFactory( locationProvider, createOrUpdateEventDetails, eventDetailResourcesProvider, + configurePeriodSelector, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt index fe1494541b..7dd1fe3bf4 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt @@ -14,17 +14,20 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels +import androidx.paging.compose.collectAsLazyPagingItems import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.parcelize.Parcelize import org.dhis2.bindings.app import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.dialogs.PeriodDialog +import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener +import org.dhis2.commons.periods.PeriodSelectorContent import org.dhis2.form.R import org.dhis2.form.model.EventMode +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog +import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import java.util.Date import javax.inject.Inject class SchedulingDialog : BottomSheetDialogFragment() { @@ -168,6 +171,7 @@ class SchedulingDialog : BottomSheetDialogFragment() { override fun onNegativeClick() { // Unused } + override fun onPositiveClick(datePicker: DatePicker) { viewModel.onDateSet( datePicker.year, @@ -181,14 +185,27 @@ class SchedulingDialog : BottomSheetDialogFragment() { } private fun showPeriodDialog() { - PeriodDialog() - .setPeriod(viewModel.eventDate.value.periodType) - .setMinDate(viewModel.eventDate.value.minDate) - .setMaxDate(viewModel.eventDate.value.maxDate) - .setPossitiveListener { selectedDate: Date -> - viewModel.setUpEventReportDate(selectedDate) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = "PeriodType title", // TODO:6660 + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { _ -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + viewModel.setUpEventReportDate(selectedDate) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } sealed interface LaunchMode : Parcelable { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index 28a9d8017b..cd9386bb39 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt @@ -2,9 +2,14 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.map +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.dhis2.commons.bindings.enrollment @@ -12,6 +17,8 @@ import org.dhis2.commons.bindings.event import org.dhis2.commons.bindings.programStage import org.dhis2.commons.date.DateUtils import org.dhis2.commons.date.toOverdueOrScheduledUiText +import org.dhis2.commons.periods.Period +import org.dhis2.commons.periods.PeriodUseCase import org.dhis2.commons.resources.DhisPeriodUtils import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager @@ -27,6 +34,7 @@ import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import java.text.SimpleDateFormat @@ -44,6 +52,8 @@ class SchedulingViewModel( private val dateUtils: DateUtils, ) : ViewModel() { + private val periodUseCase = PeriodUseCase(d2) + lateinit var repository: EventDetailsRepository lateinit var configureEventReportDate: ConfigureEventReportDate lateinit var configureEventCatCombo: ConfigureEventCatCombo @@ -98,6 +108,7 @@ class SchedulingViewModel( is LaunchMode.NewSchedule -> { launchMode.programStagesUids.mapNotNull(d2::programStage) } + is LaunchMode.EnterEvent -> emptyList() } } @@ -162,6 +173,7 @@ class SchedulingViewModel( resourceManager = resourceManager, eventResourcesProvider = eventResourcesProvider, ) + private fun loadProgramStage(event: Event? = null) { viewModelScope.launch { val selectedDate = event?.dueDate() ?: configureEventReportDate.getNextScheduleDate() @@ -303,13 +315,46 @@ class SchedulingViewModel( viewModelScope.launch { when (launchMode) { is LaunchMode.EnterEvent -> { - d2.eventModule().events().uid(launchMode.eventUid).setStatus(EventStatus.SKIPPED) + d2.eventModule().events().uid(launchMode.eventUid) + .setStatus(EventStatus.SKIPPED) onEventSkipped?.invoke(programStage.value?.displayEventLabel()) } + is LaunchMode.NewSchedule -> { // no-op } } } } + + fun fetchPeriods(): Flow> { + val programStage = programStage.value ?: return emptyFlow() + val periodType = programStage.periodType() ?: PeriodType.Daily + val enrollmentUid = enrollment.value?.uid() ?: return emptyFlow() + return with(periodUseCase) { + val unavailableDates = getEventUnavailableDates( + programStage.uid(), + enrollmentUid, + null, + ) + fetchPeriods( + periodType = periodType, + selectedDate = eventDate.value.currentDate, + initialDate = getEventPeriodMinDate( + programStage = programStage, + isScheduling = true, + eventEnrollmentUid = enrollmentUid, + ), + maxDate = getEventPeriodMaxDate( + programStage = programStage, + isScheduling = true, + eventEnrollmentUid = enrollmentUid, + ), + ).map { paging -> + paging.map { period -> + period.copy(enabled = unavailableDates.contains(period.startDate).not()) + } + } + } + } } diff --git a/commons/src/main/java/org/dhis2/commons/periods/GetPeriodLabel.kt b/commons/src/main/java/org/dhis2/commons/periods/GetPeriodLabel.kt new file mode 100644 index 0000000000..2c4a9e2366 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/GetPeriodLabel.kt @@ -0,0 +1,142 @@ +package org.dhis2.commons.periods + +import org.apache.commons.text.WordUtils +import org.hisp.dhis.android.core.period.PeriodType +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.regex.Pattern + +private const val MONTH_FULL_FORMAT = "MMMM" +private const val MONTH_DAY_SHORT_FORMAT = "MMM d" +private const val MONTH_YEAR_FULL_FORMAT = "MMMM yyyy" +private const val YEARLY_FORMAT = "yyyy" +private const val DAILY_FORMAT = "dd/MM/yyyy" +private const val FROM_TO_LABEL = "%s - %s" + +class GetPeriodLabel( + private val defaultQuarterlyLabel: String = "Q%d %s (%s - %s)", + private val defaultWeeklyLabel: String = "Week %d: %s - %s, %s", + private val defaultBiWeeklyLabel: String = "Period %d: %s - %s", +) { + operator fun invoke( + periodType: PeriodType?, + periodId: String, + periodStartDate: Date, + periodEndDate: Date, + locale: Locale, + ): String { + val formattedDate: String + when (periodType) { + PeriodType.Weekly, + PeriodType.WeeklyWednesday, + PeriodType.WeeklyThursday, + PeriodType.WeeklySaturday, + PeriodType.WeeklySunday, + -> { + formattedDate = defaultWeeklyLabel.format( + weekOfTheYear(periodType, periodId), + SimpleDateFormat(MONTH_DAY_SHORT_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_DAY_SHORT_FORMAT, locale).format(periodEndDate), + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.BiWeekly -> { + formattedDate = defaultBiWeeklyLabel.format( + weekOfTheYear(periodType, periodId), + SimpleDateFormat(DAILY_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(DAILY_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.Monthly -> + formattedDate = + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate) + + PeriodType.BiMonthly -> + formattedDate = FROM_TO_LABEL.format( + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate), + ) + + PeriodType.Quarterly, + PeriodType.QuarterlyNov, + -> { + val startYear = SimpleDateFormat(YEARLY_FORMAT, locale).format(periodStartDate) + val endYear = SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate) + val (yearFormat, initMonthFormat) = if (startYear != endYear) { + Pair( + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodEndDate), + SimpleDateFormat( + MONTH_YEAR_FULL_FORMAT, + locale, + ).format(periodStartDate), + ) + } else { + Pair( + SimpleDateFormat(YEARLY_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodStartDate), + ) + } + formattedDate = defaultQuarterlyLabel.format( + quarter(periodType, periodId), + yearFormat, + initMonthFormat, + SimpleDateFormat(MONTH_FULL_FORMAT, locale).format(periodEndDate), + ) + } + + PeriodType.SixMonthly, + PeriodType.SixMonthlyApril, + -> + formattedDate = FROM_TO_LABEL.format( + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodEndDate), + ) + + PeriodType.FinancialApril, + PeriodType.FinancialJuly, + PeriodType.FinancialOct, + -> + formattedDate = FROM_TO_LABEL.format( + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodStartDate), + SimpleDateFormat(MONTH_YEAR_FULL_FORMAT, locale).format(periodEndDate), + ) + + PeriodType.Yearly -> + formattedDate = + SimpleDateFormat( + YEARLY_FORMAT, + locale, + ).format(periodStartDate) + + else -> + formattedDate = + SimpleDateFormat(DAILY_FORMAT, locale).format(periodStartDate) + } + return WordUtils.capitalize(formattedDate) + } + + private fun weekOfTheYear(periodType: PeriodType, periodId: String): Int { + val pattern = + Pattern.compile(periodType.pattern) + val matcher = pattern.matcher(periodId) + var weekNumber = 0 + if (matcher.find()) { + weekNumber = matcher.group(2)?.toInt() ?: 0 + } + return weekNumber + } + + private fun quarter(periodType: PeriodType, periodId: String): Int { + val pattern = + Pattern.compile(periodType.pattern) + val matcher = pattern.matcher(periodId) + var quarterNumber = 0 + if (matcher.find()) { + quarterNumber = matcher.group(2)?.toInt() ?: 0 + } + return quarterNumber + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/Period.kt b/commons/src/main/java/org/dhis2/commons/periods/Period.kt new file mode 100644 index 0000000000..2d69da1a68 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/Period.kt @@ -0,0 +1,11 @@ +package org.dhis2.commons.periods + +import java.util.Date + +data class Period( + val id: String, + val name: String, + val startDate: Date, + val enabled: Boolean, + val selected: Boolean, +) diff --git a/commons/src/main/java/org/dhis2/commons/periods/PeriodSelector.kt b/commons/src/main/java/org/dhis2/commons/periods/PeriodSelector.kt new file mode 100644 index 0000000000..d75bb11bfc --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/PeriodSelector.kt @@ -0,0 +1,111 @@ +package org.dhis2.commons.periods + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing8 +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import java.util.Date + +@Composable +fun PeriodSelectorContent( + periods: LazyPagingItems, + scrollState: LazyListState, + onPeriodSelected: (Date) -> Unit, +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + state = scrollState, + ) { + when (periods.loadState.refresh) { + is LoadState.Error -> periods.retry() + LoadState.Loading -> + item { ProgressItem(contentPadding = PaddingValues(Spacing8)) } + + is LoadState.NotLoading -> + items(periods.itemCount) { index -> + val period = periods[index] + ListItem( + contentPadding = PaddingValues(Spacing8), + label = period?.name ?: "", + selected = period?.selected == true, + enabled = period?.enabled == true, + ) { + period?.startDate?.let(onPeriodSelected) + } + } + } + } +} + +@Deprecated("Expose design system item", replaceWith = ReplaceWith("DropDownItem")) +@Composable +fun ListItem( + modifier: Modifier = Modifier, + contentPadding: PaddingValues, + label: String, + selected: Boolean, + enabled: Boolean, + onItemClick: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Spacing8)) + .clickable(enabled = enabled, onClick = onItemClick) + .background( + color = if (selected) { + SurfaceColor.PrimaryContainer + } else { + Color.Unspecified + }, + ) + .padding(contentPadding), + ) { + Text( + text = label, + style = if (selected) { + MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) + } else { + MaterialTheme.typography.bodyLarge + }, + color = if (enabled) TextColor.OnSurface else TextColor.OnDisabledSurface, + ) + } +} + +@Composable +fun ProgressItem( + modifier: Modifier = Modifier, + contentPadding: PaddingValues, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(Spacing8)) + .background(color = Color.Unspecified) + .padding(contentPadding), + contentAlignment = Alignment.Center, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/PeriodSource.kt b/commons/src/main/java/org/dhis2/commons/periods/PeriodSource.kt new file mode 100644 index 0000000000..1966f1f4bb --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/PeriodSource.kt @@ -0,0 +1,73 @@ +package org.dhis2.commons.periods + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.period.internal.PeriodHelper +import java.util.Date +import java.util.Locale + +class PeriodSource( + private val periodHelper: PeriodHelper, + private val selectedDate: Date?, + private val periodType: PeriodType, + private val initialDate: Date, + private val maxDate: Date?, +) : PagingSource() { + + private val periodLabel = GetPeriodLabel() + + override suspend fun load(params: LoadParams): LoadResult { + return try { + var maxPageReached = false + val periodsPerPage = params.loadSize + val position = params.key ?: 1 + val periods: List = buildList { + repeat(periodsPerPage) { indexInPage -> + val period = periodHelper.blockingGetPeriodForPeriodTypeAndDate( + periodType, + initialDate, + position - 1 + indexInPage, + + ) + if (maxDate == null || period.startDate() + ?.before(maxDate) == true || period.startDate() == maxDate + ) { + add( + Period( + id = period.periodId()!!, + name = periodLabel( + periodType = periodType, + periodId = period.periodId()!!, + periodStartDate = period.startDate()!!, + periodEndDate = period.endDate()!!, + locale = Locale.getDefault(), + ), + startDate = period.startDate()!!, + enabled = true, + selected = period.startDate() == selectedDate, + ), + ) + } else { + maxPageReached = true + } + } + } + + LoadResult.Page( + data = periods, + prevKey = if (position == 1) null else (position - 1), + nextKey = if (maxPageReached) null else (position + 1), + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) + ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } +} diff --git a/commons/src/main/java/org/dhis2/commons/periods/PeriodUseCase.kt b/commons/src/main/java/org/dhis2/commons/periods/PeriodUseCase.kt new file mode 100644 index 0000000000..bfc2f75693 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/periods/PeriodUseCase.kt @@ -0,0 +1,123 @@ +package org.dhis2.commons.periods + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.eventsBy +import org.dhis2.commons.bindings.program +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.period.PeriodType +import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Date + +class PeriodUseCase(private val d2: D2) { + private val periodHelper = d2.periodModule().periodHelper() + fun fetchPeriods( + periodType: PeriodType, + selectedDate: Date?, + initialDate: Date, + maxDate: Date?, + ): Flow> = Pager( + config = PagingConfig(pageSize = 10, maxSize = 100), + pagingSourceFactory = { + PeriodSource( + periodHelper = periodHelper, + periodType = periodType, + initialDate = initialDate, + maxDate = maxDate, + selectedDate = selectedDate, + ) + }, + ).flow + + fun getEventPeriodMinDate( + programStage: ProgramStage, + isScheduling: Boolean, + eventEnrollmentUid: String?, + ): Date { + val periodType = programStage.periodType() ?: PeriodType.Daily + + val program = programStage.program()?.let { d2.program(it.uid()) } + + val expiryDays = program?.expiryDays() + + val currentDate = if (!isScheduling && expiryDays == null) { + val enrollment = eventEnrollmentUid?.let { d2.enrollment(it) } + if (programStage.generatedByEnrollmentDate() == true) { + enrollment?.enrollmentDate() + } else { + enrollment?.incidentDate() ?: enrollment?.enrollmentDate() + } + } else { + d2.generatePeriod(periodType, offset = 1).startDate() + } ?: Date() + + val currentPeriod = d2.generatePeriod(periodType) + val previousPeriodLastDay = + d2.generatePeriod(PeriodType.Daily, currentPeriod.startDate()!!, expiryDays ?: 0) + .startDate() + + return if (currentDate.after(previousPeriodLastDay)) { + currentPeriod.startDate() + } else { + d2.generatePeriod(periodType, offset = -1).startDate() + } ?: Date() + } + + fun getEventPeriodMaxDate( + programStage: ProgramStage, + isScheduling: Boolean, + eventEnrollmentUid: String?, + ): Date? { + if (isScheduling) return null + + val periodType = programStage.periodType() ?: PeriodType.Daily + + val program = programStage.program()?.let { d2.program(it.uid()) } + + val expiryDays = program?.expiryDays() + + val currentDate = if (expiryDays == null) { + val enrollment = eventEnrollmentUid?.let { d2.enrollment(it) } + if (programStage.generatedByEnrollmentDate() == true) { + enrollment?.enrollmentDate() + } else { + enrollment?.incidentDate() ?: enrollment?.enrollmentDate() + } + } else { + Date() + } ?: Date() + + val currentPeriod = d2.generatePeriod(periodType, currentDate) + + return currentPeriod.startDate() + } + + fun getEventUnavailableDates( + programStageUid: String, + enrollmentUid: String?, + currentEventUid: String?, + ): List { + val enrollment = enrollmentUid?.let { d2.enrollment(it) } + return d2.eventsBy(enrollmentUid = enrollment?.uid()).mapNotNull { + if (it.programStage() == programStageUid && + (currentEventUid == null || it.uid() != currentEventUid) && + it.status() != EventStatus.SKIPPED + ) { + it.eventDate() + } else { + null + } + } + } +} + +private fun D2.generatePeriod( + periodType: PeriodType, + date: Date = Date(), + offset: Int = 0, +) = periodModule().periodHelper() + .blockingGetPeriodForPeriodTypeAndDate(periodType, date, offset) diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt index 83295fea20..3f2e0ed382 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryBaseRepository.kt @@ -1,5 +1,9 @@ package org.dhis2.form.data +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.dhis2.commons.periods.Period import org.dhis2.form.data.metadata.FormBaseConfiguration import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.SectionUiModelImpl @@ -77,6 +81,8 @@ abstract class DataEntryBaseRepository( return optionsFromGroups } + override fun fetchPeriods(): Flow> = emptyFlow() + override fun dateFormatConfiguration(): String? { return conf.dateFormatConfiguration() } diff --git a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt index 4d54b1e408..ca329289a2 100644 --- a/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/DataEntryRepository.kt @@ -1,6 +1,9 @@ package org.dhis2.form.data +import androidx.paging.PagingData import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.Period import org.dhis2.form.model.EventMode import org.dhis2.form.model.FieldUiModel import org.hisp.dhis.android.core.common.ValidationStrategy @@ -37,4 +40,5 @@ interface DataEntryRepository { fun disableCollapsableSections(): Boolean? fun getSpecificDataEntryItems(uid: String): List + fun fetchPeriods(): Flow> } diff --git a/form/src/main/java/org/dhis2/form/data/EventRepository.kt b/form/src/main/java/org/dhis2/form/data/EventRepository.kt index 57164c7278..03e78f19d5 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -1,15 +1,24 @@ package org.dhis2.form.data import android.text.TextUtils +import androidx.paging.PagingData +import androidx.paging.map import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import org.dhis2.bindings.blockingGetValueCheck import org.dhis2.bindings.userFriendlyValue +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.eventsBy import org.dhis2.commons.bindings.program import org.dhis2.commons.date.DateUtils import org.dhis2.commons.extensions.inDateRange import org.dhis2.commons.extensions.inOrgUnit import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.Period +import org.dhis2.commons.periods.PeriodUseCase import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager @@ -55,6 +64,8 @@ class EventRepository( private val eventMode: EventMode, ) : DataEntryBaseRepository(FormBaseConfiguration(d2), fieldFactory) { + private val periodUseCase = PeriodUseCase(d2) + private var event = d2.eventModule().events().uid(eventUid).blockingGet() private val programStage by lazy { @@ -183,7 +194,7 @@ class EventRepository( return true } - override fun eventMode(): EventMode? { + override fun eventMode(): EventMode { return eventMode } @@ -426,7 +437,7 @@ class EventRepository( if (periodType != PeriodType.Daily) { PeriodSelector( type = periodType, - minDate = getPeriodMinDate(periodType), + minDate = null, maxDate = null, ) } else { @@ -435,97 +446,20 @@ class EventRepository( } } - private fun D2.generatePeriod( - periodType: PeriodType, - date: Date = Date(), - offset: Int = 0, - ) = d2.periodModule().periodHelper() - .blockingGetPeriodForPeriodTypeAndDate(periodType, date, offset) - - private fun getPeriodMinDate(periodType: PeriodType): Date? { - val program = programUid?.let { d2.program(it) } - - val expiryDays = program?.expiryDays() - - return if (expiryDays == null) { - null - } else { - val currentDate = Date() - val currentPeriod = d2.generatePeriod(periodType) - val previousPeriodLastDay = - d2.generatePeriod(PeriodType.Daily, currentPeriod.startDate()!!, expiryDays) - .startDate() - if (currentDate.after(previousPeriodLastDay)) { - currentDate + private fun getUnavailableDates(): List { + val enrollment = event?.enrollment()?.let { d2.enrollment(it) } + return d2.eventsBy(enrollmentUid = enrollment?.uid()).mapNotNull { + if (it.programStage() == programStage?.uid() && + it.uid() != eventUid && + it.status() != EventStatus.SKIPPED + ) { + it.eventDate() } else { - d2.generatePeriod(periodType, offset = -1).startDate() + null } } } - /* private fun getFirstAvailablePeriod(enrollmentUid: String?, programStage: ProgramStage?): Date { - val stageLastDate = getStageLastDate() - val minEventDate = stageLastDate ?: when (programStage?.generatedByEnrollmentDate()) { - true -> getEnrollmentDate(enrollmentUid) - else -> getEnrollmentIncidentDate(enrollmentUid) ?: getEnrollmentDate(enrollmentUid) - } - val calendar = DateUtils.getInstance().getCalendarByDate(minEventDate) - - return dateUtils.getNextPeriod( - */ - /* period = */ - /* programStage?.periodType(), - */ - /* currentDate = */ - /* calendar.time ?: event?.eventDate(), - */ - /* page = */ - /* if (stageLastDate == null) 0 else 1 - ) - }*/ - - /*private fun getStageLastDate(): Date? { - val enrollmentUid = event?.enrollment() - val programStageUid = programStage?.uid() - val activeEvents = - d2.eventModule().events().byEnrollmentUid() - .eq(enrollmentUid).byProgramStageUid() - .eq(programStageUid) - .byDeleted().isFalse - .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet() - .filter { it.uid() != eventUid } - val scheduleEvents = - d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).byProgramStageUid() - .eq(programStageUid) - .byDeleted().isFalse - .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet() - .filter { it.uid() != eventUid } - - var activeDate: Date? = null - var scheduleDate: Date? = null - if (activeEvents.isNotEmpty()) { - activeDate = activeEvents[0].eventDate() - } - if (scheduleEvents.isNotEmpty()) scheduleDate = scheduleEvents[0].dueDate() - - return when { - scheduleDate == null -> activeDate - activeDate == null -> scheduleDate - activeDate.before(scheduleDate) -> scheduleDate - else -> activeDate - } - } - - private fun getEnrollmentDate(uid: String?): Date? { - val enrollment = d2.enrollmentModule().enrollments().byUid().eq(uid).blockingGet().first() - return enrollment.enrollmentDate() - } - - private fun getEnrollmentIncidentDate(uid: String?): Date? { - val enrollment = d2.enrollmentModule().enrollments().uid(uid).blockingGet() - return enrollment?.incidentDate() - }*/ - private fun createEventDetailsSection(): FieldUiModel { return fieldFactory.createSection( sectionUid = EVENT_DETAILS_SECTION_UID, @@ -556,6 +490,37 @@ class EventRepository( } } + override fun fetchPeriods(): Flow> { + val unavailableDates = getUnavailableDates() + val periodType = programStage?.periodType() ?: PeriodType.Daily + val stage = programStage ?: return flowOf() + val eventEnrollmentUid = event?.enrollment() ?: return flowOf() + return with(periodUseCase) { + fetchPeriods( + periodType = periodType, + selectedDate = if (eventMode == EventMode.SCHEDULE) { + event?.dueDate() + } else { + event?.eventDate() + }, + initialDate = getEventPeriodMinDate( + programStage = stage, + isScheduling = eventMode == EventMode.SCHEDULE, + eventEnrollmentUid = eventEnrollmentUid, + ), + maxDate = getEventPeriodMaxDate( + programStage = stage, + isScheduling = eventMode == EventMode.SCHEDULE, + eventEnrollmentUid = eventEnrollmentUid, + ), + ).map { paging -> + paging.map { period -> + period.copy(enabled = unavailableDates.contains(period.startDate).not()) + } + } + } + } + private fun getFieldsForSingleSection(): Single> { return Single.fromCallable { val stageDataElements = diff --git a/form/src/main/java/org/dhis2/form/data/FormRepository.kt b/form/src/main/java/org/dhis2/form/data/FormRepository.kt index 37203fc693..cb01f754bb 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepository.kt @@ -1,5 +1,8 @@ package org.dhis2.form.data +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.Period import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.RowAction import org.dhis2.form.model.StoreResult @@ -32,4 +35,5 @@ interface FormRepository { fun getListFromPreferences(uid: String): MutableList fun saveListToPreferences(uid: String, list: List) fun activateEvent() + fun fetchPeriods(): Flow> } diff --git a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt index 12bc763b88..00f8651abb 100644 --- a/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/data/FormRepositoryImpl.kt @@ -1,7 +1,10 @@ package org.dhis2.form.data +import androidx.paging.PagingData import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.flow.Flow +import org.dhis2.commons.periods.Period import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.form.data.EnrollmentRepository.Companion.ENROLLMENT_DATE_UID @@ -92,6 +95,10 @@ class FormRepositoryImpl( formValueStore.activateEvent() } + override fun fetchPeriods(): Flow> { + return dataEntryRepository.fetchPeriods() + } + private fun List.setLastItem(): List { if (isEmpty()) { return this diff --git a/form/src/main/java/org/dhis2/form/ui/FormView.kt b/form/src/main/java/org/dhis2/form/ui/FormView.kt index f5997abe9e..91d9b94a94 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormView.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormView.kt @@ -28,6 +28,7 @@ import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels +import androidx.paging.compose.collectAsLazyPagingItems import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.journeyapps.barcodescanner.ScanOptions import org.dhis2.commons.ActivityResultObservable @@ -41,7 +42,6 @@ import org.dhis2.commons.data.FormFileProvider import org.dhis2.commons.date.DateUtils import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.CustomDialog -import org.dhis2.commons.dialogs.PeriodDialog import org.dhis2.commons.extensions.closeKeyboard import org.dhis2.commons.extensions.serializable import org.dhis2.commons.extensions.truncate @@ -49,6 +49,7 @@ import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.periods.PeriodSelectorContent import org.dhis2.form.R import org.dhis2.form.data.DataIntegrityCheckResult import org.dhis2.form.data.FieldsWithErrorResult @@ -89,7 +90,6 @@ import org.hisp.dhis.android.core.common.ValueTypeRenderingType import org.hisp.dhis.android.core.event.EventStatus import timber.log.Timber import java.io.File -import java.util.Date class FormView : Fragment() { @@ -483,7 +483,7 @@ class FormView : Fragment() { ) }, showDivider = fieldsWithIssues.isNotEmpty(), - content = { bottomSheetDialog -> + content = { bottomSheetDialog, _ -> DialogContent(fieldsWithIssues, bottomSheetDialog = bottomSheetDialog) }, ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) @@ -635,22 +635,34 @@ class FormView : Fragment() { } private fun showPeriodDialog(uiEvent: RecyclerViewUiEvents.SelectPeriod) { - PeriodDialog() - .setTitle(uiEvent.title) - .setPeriod(uiEvent.periodType) - .setMinDate(uiEvent.minDate) - .setMaxDate(uiEvent.maxDate) - .setPossitiveListener { selectedDate: Date -> - val dateString = DateUtils.oldUiDateFormat().format(selectedDate) - intentHandler( - FormIntent.OnSave( - uiEvent.uid, - dateString, - ValueType.DATE, - ), - ) - } - .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) + BottomSheetDialog( + bottomSheetDialogUiModel = BottomSheetDialogUiModel( + title = uiEvent.title, + iconResource = -1, + ), + onSecondaryButtonClicked = { + }, + onMainButtonClicked = { bottomSheetDialog -> + }, + showDivider = true, + content = { bottomSheetDialog, scrollState -> + val periods = viewModel.fetchPeriods().collectAsLazyPagingItems() + PeriodSelectorContent( + periods = periods, + scrollState = scrollState, + ) { selectedDate -> + val dateString = DateUtils.oldUiDateFormat().format(selectedDate) + intentHandler( + FormIntent.OnSave( + uiEvent.uid, + dateString, + ValueType.DATE, + ), + ) + bottomSheetDialog.dismiss() + } + }, + ).show(childFragmentManager, AlertBottomDialog::class.java.simpleName) } private fun openChooserIntent(uiEvent: RecyclerViewUiEvents.OpenChooserIntent) { diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt index dfa03988b6..c19e3add63 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -6,16 +6,19 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import kotlinx.coroutines.async import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.periods.Period import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.R import org.dhis2.form.data.DataIntegrityCheckResult @@ -845,6 +848,10 @@ class FormViewModel( } } + fun fetchPeriods(): Flow> { + return repository.fetchPeriods().flowOn(dispatcher.io()) + } + companion object { const val TAG = "FormViewModel" } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt index 860ae6fbd7..b56bc9a091 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/PeriodSelectorProvider.kt @@ -7,9 +7,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.res.stringResource -import org.dhis2.commons.date.DateUtils -import org.dhis2.form.R import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText @@ -17,10 +14,7 @@ import org.dhis2.form.model.FieldUiModel import org.dhis2.form.ui.event.RecyclerViewUiEvents import org.hisp.dhis.mobile.ui.designsystem.component.DropdownInputField import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem -import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown -import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle -import java.util.Date @Composable fun ProvidePeriodSelector( @@ -30,74 +24,38 @@ fun ProvidePeriodSelector( focusRequester: FocusRequester, uiEventHandler: (RecyclerViewUiEvents) -> Unit, ) { - val currentDate = DateUtils.getInstance().getStartOfDay(Date()) - if ((fieldUiModel.periodSelector?.minDate?.after(currentDate) == true)) { - ProvideEmptyPeriodSelector( - modifier = modifier, - name = fieldUiModel.label, - inputStyle = inputStyle, + var selectedItem by remember(fieldUiModel.displayName) { + mutableStateOf( + fieldUiModel.displayName, ) - } else { - var selectedItem by remember(fieldUiModel.displayName) { - mutableStateOf( - fieldUiModel.displayName, - ) - } - - DropdownInputField( - modifier = modifier, - title = fieldUiModel.label, - state = fieldUiModel.inputState(), - inputStyle = inputStyle, - legendData = fieldUiModel.legend(), - supportingTextData = fieldUiModel.supportingText(), - isRequiredField = fieldUiModel.mandatory, - selectedItem = DropdownItem(selectedItem ?: ""), - onResetButtonClicked = { - selectedItem = null - fieldUiModel.onClear() - }, - onDropdownIconClick = { - uiEventHandler( - RecyclerViewUiEvents.SelectPeriod( - uid = fieldUiModel.uid, - title = fieldUiModel.label, - periodType = fieldUiModel.periodSelector!!.type, - minDate = fieldUiModel.periodSelector!!.minDate, - maxDate = fieldUiModel.periodSelector!!.maxDate, - ), - ) - }, - onFocusChanged = {}, - focusRequester = focusRequester, - expanded = false, - ) - } -} - -@Composable -fun ProvideEmptyPeriodSelector( - modifier: Modifier = Modifier, - name: String, - inputStyle: InputStyle, -) { - var selectedItem by remember { - mutableStateOf("") } - InputDropDown( + DropdownInputField( modifier = modifier, - title = name, - state = InputShellState.UNFOCUSED, + title = fieldUiModel.label, + state = fieldUiModel.inputState(), inputStyle = inputStyle, - selectedItem = DropdownItem(selectedItem), + legendData = fieldUiModel.legend(), + supportingTextData = fieldUiModel.supportingText(), + isRequiredField = fieldUiModel.mandatory, + selectedItem = selectedItem?.let { DropdownItem(it) }, onResetButtonClicked = { - selectedItem = "" + selectedItem = null + fieldUiModel.onClear() }, - onItemSelected = { newSelectedDropdownItem -> - selectedItem = newSelectedDropdownItem.label + onDropdownIconClick = { + uiEventHandler( + RecyclerViewUiEvents.SelectPeriod( + uid = fieldUiModel.uid, + title = fieldUiModel.label, + periodType = fieldUiModel.periodSelector!!.type, + minDate = fieldUiModel.periodSelector!!.minDate, + maxDate = fieldUiModel.periodSelector!!.maxDate, + ), + ) }, - dropdownItems = listOf(DropdownItem(stringResource(id = R.string.no_periods))), - isRequiredField = false, + onFocusChanged = {}, + focusRequester = focusRequester, + expanded = false, ) } diff --git a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt index 1b03decc2b..a6eb05d4b6 100644 --- a/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt +++ b/ui-components/src/main/java/org/dhis2/ui/dialogs/bottomsheet/BottomSheetDialog.kt @@ -9,7 +9,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -47,7 +48,7 @@ class BottomSheetDialog( var onMessageClick: () -> Unit = {}, val showDivider: Boolean = false, val content: @Composable - ((org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog) -> Unit)? = null, + ((org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog, scrollState: LazyListState) -> Unit)? = null, ) : BottomSheetDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { @@ -71,6 +72,7 @@ class BottomSheetDialog( setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { DHIS2Theme { + val scrollState = rememberLazyListState() BottomSheetShell( title = bottomSheetDialogUiModel.title, description = when (bottomSheetDialogUiModel.clickableWord) { @@ -79,12 +81,14 @@ class BottomSheetDialog( }, headerTextAlignment = bottomSheetDialogUiModel.headerTextAlignment, icon = { - Icon( - modifier = Modifier.size(Spacing24), - painter = painterResource(bottomSheetDialogUiModel.iconResource), - contentDescription = "Icon", - tint = SurfaceColor.Primary, - ) + if (bottomSheetDialogUiModel.iconResource != -1) { + Icon( + modifier = Modifier.size(Spacing24), + painter = painterResource(bottomSheetDialogUiModel.iconResource), + contentDescription = "Icon", + tint = SurfaceColor.Primary, + ) + } }, showSectionDivider = showDivider, buttonBlock = { @@ -131,14 +135,14 @@ class BottomSheetDialog( }, content = { if (content != null) { - content.invoke(this@BottomSheetDialog) + content.invoke(this@BottomSheetDialog, scrollState) } else { bottomSheetDialogUiModel.clickableWord?.let { ClickableTextContent(bottomSheetDialogUiModel.message ?: "", it) } } }, - contentScrollState = rememberScrollState(), + contentScrollState = scrollState, ) } }