diff --git a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/components/DateSettingSection.kt b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/components/DateSettingSection.kt index 6681dd87..f3b7ce9d 100644 --- a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/components/DateSettingSection.kt +++ b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/components/DateSettingSection.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -41,7 +42,6 @@ fun DateSettingSection( today: LocalDate, selectedTime: LocalTime?, isActiveDial: Boolean, - interactionSource: MutableInteractionSource, onToggleCalendar: () -> Unit, onToggleDial: () -> Unit, onPrevMonth: () -> Unit, @@ -63,7 +63,6 @@ fun DateSettingSection( DateSectionHeader( selectedDate = selectedDate, isActiveCalendar = isActiveCalendar, - interactionSource = interactionSource, isRepeat = isRepeat, activeWeekDays = activeWeekDays, onToggleCalendar = onToggleCalendar, @@ -97,7 +96,6 @@ fun DateSettingSection( TimeSectionHeader( selectedTime = selectedTime, isActiveDial = isActiveDial, - interactionSource = interactionSource, onToggleDial = onToggleDial, ) @@ -120,9 +118,10 @@ fun DateSettingSection( fun TimeSectionHeader( selectedTime: LocalTime?, isActiveDial: Boolean, - interactionSource: MutableInteractionSource, onToggleDial: () -> Unit, ) { + val interactionSource = remember { MutableInteractionSource() } + Row( modifier = Modifier @@ -154,11 +153,12 @@ fun TimeSectionHeader( fun DateSectionHeader( selectedDate: LocalDate?, isActiveCalendar: Boolean, - interactionSource: MutableInteractionSource, isRepeat: Boolean, activeWeekDays: Set, onToggleCalendar: () -> Unit, ) { + val interactionSource = remember { MutableInteractionSource() } + Row( modifier = Modifier diff --git a/core/navigation/src/commonMain/kotlin/com/ondot/navigation/AppNavHost.kt b/core/navigation/src/commonMain/kotlin/com/ondot/navigation/AppNavHost.kt index 97978f96..3c69ed4c 100644 --- a/core/navigation/src/commonMain/kotlin/com/ondot/navigation/AppNavHost.kt +++ b/core/navigation/src/commonMain/kotlin/com/ondot/navigation/AppNavHost.kt @@ -117,6 +117,10 @@ private fun String.toScreenViewEventName(): String = NavRoutes.PlacePicker.route -> "screen_view_place_picker" NavRoutes.RouteLoading.route -> "screen_view_route_loading" NavRoutes.CheckSchedule.route -> "screen_view_check_schedule" + NavRoutes.ScheduleRepeatSettingMvi.route -> "screen_view_schedule_repeat_setting" + NavRoutes.PlacePickerMvi.route -> "screen_view_place_picker" + NavRoutes.RouteLoadingMvi.route -> "screen_view_route_loading" + NavRoutes.CheckScheduleMvi.route -> "screen_view_check_schedule" NavRoutes.DeleteAccount.route -> "screen_view_delete_account" NavRoutes.HomeAddressSetting.route -> "screen_view_home_address_setting" diff --git a/core/navigation/src/commonMain/kotlin/com/ondot/navigation/NavRoutes.kt b/core/navigation/src/commonMain/kotlin/com/ondot/navigation/NavRoutes.kt index be554b9e..30b09b2d 100644 --- a/core/navigation/src/commonMain/kotlin/com/ondot/navigation/NavRoutes.kt +++ b/core/navigation/src/commonMain/kotlin/com/ondot/navigation/NavRoutes.kt @@ -76,6 +76,17 @@ sealed class NavRoutes( data object CheckSchedule : NavRoutes("checkSchedule") + // General Mvi + data object GeneralScheduleMviGraph : NavRoutes("generalScheduleMviGraph") + + data object ScheduleRepeatSettingMvi : NavRoutes("scheduleRepeatSettingMvi") + + data object PlacePickerMvi : NavRoutes("placePickerMvi") + + data object RouteLoadingMvi : NavRoutes("routeLoadingMvi") + + data object CheckScheduleMvi : NavRoutes("checkScheduleMvi") + // EditSchedule @Serializable data object EditScheduleGraph : NavRoutes("editScheduleGraph") diff --git a/data/src/commonMain/kotlin/com/ondot/data/repository/PlaceRepositoryImpl.kt b/data/src/commonMain/kotlin/com/ondot/data/repository/PlaceRepositoryImpl.kt index b4929c11..2c1e75e8 100644 --- a/data/src/commonMain/kotlin/com/ondot/data/repository/PlaceRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/com/ondot/data/repository/PlaceRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.ondot.data.repository import com.ondot.data.mapper.AddressListResponseMapper import com.ondot.data.mapper.PlaceHistoryResponseMapper +import com.ondot.data.model.response.member.AddressResponse import com.ondot.data.model.response.member.PlaceHistoryResponse import com.ondot.data.model.response.member.mapper.toDomain import com.ondot.domain.model.member.AddressInfo @@ -47,6 +48,16 @@ class PlaceRepositoryImpl( emit(fetch(HttpMethod.DELETE, "/places/history", body = request)) } + override suspend fun searchPlaceAppResult(query: String): AppResult> = + safeApiCall { + networkClient + .requestOrThrow>( + method = HttpMethod.GET, + path = "/places/search", + queryParams = mapOf("query" to query), + ).let(AddressListResponseMapper::responseToModel) + } + override suspend fun deleteHistory(searchedAt: String): AppResult = safeApiCall { networkClient.requestOrThrow( diff --git a/data/src/commonMain/kotlin/com/ondot/data/repository/ScheduleRepositoryImpl.kt b/data/src/commonMain/kotlin/com/ondot/data/repository/ScheduleRepositoryImpl.kt index e274a450..727b6a4f 100644 --- a/data/src/commonMain/kotlin/com/ondot/data/repository/ScheduleRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/com/ondot/data/repository/ScheduleRepositoryImpl.kt @@ -7,6 +7,7 @@ import com.ondot.data.mapper.SchedulePreparationResponseMapper import com.ondot.data.model.request.everytime.EverytimeValidateRequest import com.ondot.data.model.request.everytime.mapper.toRequest import com.ondot.data.model.response.schedule.EverytimeValidateResponse +import com.ondot.data.model.response.schedule.ScheduleAlarmResponse import com.ondot.data.model.response.schedule.mapper.toDomain import com.ondot.domain.datasource.ScheduleLocalDataSource import com.ondot.domain.model.command.CreateEverytimeScheduleCommand @@ -107,6 +108,25 @@ class ScheduleRepositoryImpl( ) } + override suspend fun fetchScheduleAlarms(request: ScheduleAlarmRequest): AppResult = + safeApiCall { + networkClient + .requestOrThrow( + method = HttpMethod.POST, + path = "/alarms/setting", + body = request, + ).let(ScheduleAlarmResponseMapper::responseToModel) + } + + override suspend fun createScheduleAppResult(request: CreateScheduleRequest): AppResult = + safeApiCall { + networkClient.requestOrThrow( + method = HttpMethod.POST, + path = "/schedules", + body = request, + ) + } + override suspend fun toggleAlarm( scheduleId: Long, isEnabled: Boolean, diff --git a/domain/src/commonMain/kotlin/com/ondot/domain/repository/PlaceRepository.kt b/domain/src/commonMain/kotlin/com/ondot/domain/repository/PlaceRepository.kt index 18562762..d9eda62f 100644 --- a/domain/src/commonMain/kotlin/com/ondot/domain/repository/PlaceRepository.kt +++ b/domain/src/commonMain/kotlin/com/ondot/domain/repository/PlaceRepository.kt @@ -16,6 +16,8 @@ interface PlaceRepository { suspend fun deletePlaceHistory(request: DeletePlaceHistoryRequest): Flow> // -----------MVI + suspend fun searchPlaceAppResult(query: String): AppResult> + suspend fun deleteHistory(searchedAt: String): AppResult suspend fun fetchHistory(): AppResult> diff --git a/domain/src/commonMain/kotlin/com/ondot/domain/repository/ScheduleRepository.kt b/domain/src/commonMain/kotlin/com/ondot/domain/repository/ScheduleRepository.kt index d2984b7f..94db71d2 100644 --- a/domain/src/commonMain/kotlin/com/ondot/domain/repository/ScheduleRepository.kt +++ b/domain/src/commonMain/kotlin/com/ondot/domain/repository/ScheduleRepository.kt @@ -43,6 +43,10 @@ interface ScheduleRepository { suspend fun createEverytimeSchedule(command: CreateEverytimeScheduleCommand): AppResult + suspend fun fetchScheduleAlarms(request: ScheduleAlarmRequest): AppResult + + suspend fun createScheduleAppResult(request: CreateScheduleRequest): AppResult + suspend fun toggleAlarm( scheduleId: Long, isEnabled: Boolean, diff --git a/domain/testing/src/commonMain/kotlin/com/ondot/testing/fake/FakeScheduleRepository.kt b/domain/testing/src/commonMain/kotlin/com/ondot/testing/fake/FakeScheduleRepository.kt index 7dc2de65..6de7e4e4 100644 --- a/domain/testing/src/commonMain/kotlin/com/ondot/testing/fake/FakeScheduleRepository.kt +++ b/domain/testing/src/commonMain/kotlin/com/ondot/testing/fake/FakeScheduleRepository.kt @@ -152,6 +152,29 @@ class FakeScheduleRepository : ScheduleRepository { TODO("Not yet implemented") } + override suspend fun fetchScheduleAlarms(request: ScheduleAlarmRequest): AppResult = + AppResult.Success(ScheduleAlarm(preparationAlarm = Alarm(alarmId = 1), departureAlarm = Alarm(alarmId = 2))) + + override suspend fun createScheduleAppResult(request: CreateScheduleRequest): AppResult { + val newId = (scheduleMap.keys.maxOrNull() ?: 0L) + 1L + + val schedule = + Schedule( + scheduleId = newId, + scheduleTitle = request.title, + appointmentAt = request.appointmentAt, + repeatDays = request.repeatDays, + preparationNote = request.preparationNote, + hasActiveAlarm = false, + departureAlarm = request.departureAlarm, + preparationAlarm = request.preparationAlarm, + ) + + scheduleMap[newId] = schedule + + return AppResult.Success(Unit) + } + override suspend fun toggleAlarm( scheduleId: Long, isEnabled: Boolean, diff --git a/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditDateBottomSheet.kt b/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditDateBottomSheet.kt index bffe9d44..d1425de7 100644 --- a/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditDateBottomSheet.kt +++ b/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditDateBottomSheet.kt @@ -1,6 +1,5 @@ package com.ondot.edit.bottomSheet -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -10,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -35,7 +33,6 @@ fun EditDateBottomSheet( ) { val viewModel: EditBottomSheetViewModel = viewModel { EditBottomSheetViewModel() } val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val interactionSource = remember { MutableInteractionSource() } LaunchedEffect(Unit) { viewModel.initDate(isRepeat, repeatDays, currentDate) @@ -71,7 +68,6 @@ fun EditDateBottomSheet( DateSectionHeader( selectedDate = uiState.currentDate, - interactionSource = interactionSource, isActiveCalendar = true, isRepeat = uiState.isRepeat, activeWeekDays = uiState.repeatDays, diff --git a/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditTimeBottomSheet.kt b/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditTimeBottomSheet.kt index 86c6026b..bb22c39c 100644 --- a/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditTimeBottomSheet.kt +++ b/feature/edit/src/commonMain/kotlin/com/ondot/edit/bottomSheet/EditTimeBottomSheet.kt @@ -1,6 +1,5 @@ package com.ondot.edit.bottomSheet -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -12,7 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -46,7 +44,6 @@ fun EditTimeBottomSheet( ) { val viewModel: EditBottomSheetViewModel = viewModel { EditBottomSheetViewModel() } val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val interactionSource = remember { MutableInteractionSource() } val periodState = rememberLazyListState(initialFirstVisibleItemIndex = 0) val hourState = rememberLazyListState(initialFirstVisibleItemIndex = currentTime.hour.coerceIn(0, 23)) @@ -78,7 +75,6 @@ fun EditTimeBottomSheet( isActiveCalendar = uiState.isActiveCalendar, isRepeat = false, activeWeekDays = emptySet(), - interactionSource = interactionSource, onToggleCalendar = viewModel::onToggleCalendar, ) @@ -110,7 +106,6 @@ fun EditTimeBottomSheet( TimeSectionHeader( selectedTime = uiState.currentTime, isActiveDial = true, - interactionSource = interactionSource, onToggleDial = {}, ) diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/check/CheckScheduleScreen.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/check/CheckScheduleScreen.kt index dc5bd54d..7498ec76 100644 --- a/feature/general/src/commonMain/kotlin/com/ondot/general/check/CheckScheduleScreen.kt +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/check/CheckScheduleScreen.kt @@ -91,8 +91,6 @@ fun CheckScheduleScreen( CheckScheduleContent( uiState = uiState, - departurePlaceInput = uiState.placePickerState.departurePlaceInput, - arrivalPlaceInput = uiState.placePickerState.arrivalPlaceInput, focusRequester = focusRequest, onClickBack = popScreen, onCreateSchedule = viewModel::createSchedule, @@ -106,8 +104,6 @@ fun CheckScheduleScreen( @Composable fun CheckScheduleContent( uiState: GeneralScheduleUiState, - departurePlaceInput: String, - arrivalPlaceInput: String, focusRequester: FocusRequester, onClickBack: () -> Unit, onCreateSchedule: (Boolean, String) -> Unit, @@ -150,8 +146,8 @@ fun CheckScheduleContent( Spacer(modifier = Modifier.height(16.dp)) RouteInputSection( - departurePlaceInput = departurePlaceInput, - arrivalPlaceInput = arrivalPlaceInput, + departurePlaceInput = uiState.placePickerState.departurePlaceInput, + arrivalPlaceInput = uiState.placePickerState.arrivalPlaceInput, readOnly = true, ) diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleIntent.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleIntent.kt new file mode 100644 index 00000000..2c655a42 --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleIntent.kt @@ -0,0 +1,89 @@ +package com.ondot.general.contract + +import com.ondot.domain.model.enums.RouterType +import com.ondot.domain.model.member.AddressInfo +import com.ondot.domain.model.member.PlaceHistory +import com.ondot.ui.base.mvi.Intent +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime + +sealed interface GeneralScheduleIntent : Intent { + data object InitStep : GeneralScheduleIntent + + data class ToggleRepeat( + val isRepeat: Boolean, + ) : GeneralScheduleIntent + + data class SelectRepeatPreset( + val index: Int, + ) : GeneralScheduleIntent + + data class ToggleWeekDay( + val index: Int, + ) : GeneralScheduleIntent + + data object ToggleCalendar : GeneralScheduleIntent + + data object MoveToPreviousMonth : GeneralScheduleIntent + + data object MoveToNextMonth : GeneralScheduleIntent + + data class SelectDate( + val date: LocalDate, + ) : GeneralScheduleIntent + + data object ToggleTimeDial : GeneralScheduleIntent + + data class SelectTime( + val time: LocalTime, + ) : GeneralScheduleIntent + + data object InitHomeAddress : GeneralScheduleIntent + + data object InitPlaceHistory : GeneralScheduleIntent + + data class SetFocusedRouterType( + val type: RouterType, + ) : GeneralScheduleIntent + + data class UpdateRouteInput( + val input: String, + ) : GeneralScheduleIntent + + data class SelectPlace( + val place: AddressInfo, + ) : GeneralScheduleIntent + + data class SelectHistory( + val history: PlaceHistory, + ) : GeneralScheduleIntent + + data class DeleteHistory( + val history: PlaceHistory, + ) : GeneralScheduleIntent + + data object ToggleHomeDeparture : GeneralScheduleIntent + + data class SetInitialPlacePicker( + val isInitial: Boolean, + ) : GeneralScheduleIntent + + data object ClickNext : GeneralScheduleIntent + + data object ClickBack : GeneralScheduleIntent + + data class UpdateScheduleTitle( + val title: String, + ) : GeneralScheduleIntent + + data object TogglePreparationAlarm : GeneralScheduleIntent + + data class SetBottomSheetVisible( + val visible: Boolean, + ) : GeneralScheduleIntent + + data class CreateSchedule( + val isMedicationRequired: Boolean, + val preparationNote: String, + ) : GeneralScheduleIntent +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleSideEffect.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleSideEffect.kt new file mode 100644 index 00000000..a0a6e233 --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleSideEffect.kt @@ -0,0 +1,19 @@ +package com.ondot.general.contract + +import com.ondot.domain.model.enums.ToastType +import com.ondot.ui.base.mvi.SideEffect + +sealed interface GeneralScheduleSideEffect : SideEffect { + data class ShowToast( + val message: String, + val type: ToastType, + ) : GeneralScheduleSideEffect + + data object NavigateToPlacePicker : GeneralScheduleSideEffect + + data object NavigateToRouteLoading : GeneralScheduleSideEffect + + data object NavigateToMain : GeneralScheduleSideEffect + + data object RequestArrivalFocus : GeneralScheduleSideEffect +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleState.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleState.kt new file mode 100644 index 00000000..de560545 --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleState.kt @@ -0,0 +1,84 @@ +package com.ondot.general.contract + +import androidx.compose.runtime.Immutable +import com.dh.ondot.presentation.ui.theme.NEW_SCHEDULE_LABEL +import com.ondot.domain.model.alarm.Alarm +import com.ondot.general.GeneralScheduleUiState +import com.ondot.ui.base.UiState +import com.ondot.ui.screen.placepicker.model.PlacePickerUiModel +import com.ondot.util.DateTimeFormatter +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +private fun today(): LocalDate = + Clock.System + .now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + +@Immutable +data class GeneralScheduleState( + val currentStep: Int = 0, + val totalStep: Int = 0, + val isRepeat: Boolean = false, + val activeCheckChip: Int? = null, + val activeWeekDays: Set = emptySet(), + val isActiveCalendar: Boolean = true, + val isActiveDial: Boolean = false, + val calendarMonth: LocalDate = today().let { LocalDate(it.year, it.month, 1) }, + val selectedDate: LocalDate? = null, + val selectedTime: LocalTime? = null, + val today: LocalDate = today(), + val isInitialPlacePicker: Boolean = true, + val isHomeAddressInitialized: Boolean = false, + val placePickerState: PlacePickerUiModel = PlacePickerUiModel(), + val preparationAlarm: Alarm = Alarm(), + val departureAlarm: Alarm = Alarm(), + val scheduleTitle: String = NEW_SCHEDULE_LABEL, + val showBottomSheet: Boolean = false, +) : UiState { + val isRepeatStepButtonEnabled: Boolean + get() = selectedTime != null && (selectedDate != null || activeWeekDays.isNotEmpty()) + + val isPlacePickerButtonEnabled: Boolean + get() = placePickerState.selectedDeparturePlace != null && placePickerState.selectedArrivalPlace != null + + val isCurrentStepButtonEnabled: Boolean + get() = + when (currentStep) { + 1 -> isRepeatStepButtonEnabled + 2 -> isPlacePickerButtonEnabled + else -> false + } + + companion object { + fun formattedDate(date: String) = DateTimeFormatter.formatKoreanDateMonthDay(date) + } +} + +fun GeneralScheduleState.toLegacyUiState(): GeneralScheduleUiState = + GeneralScheduleUiState( + currentStep = currentStep, + totalStep = totalStep, + isRepeat = isRepeat, + activeCheckChip = activeCheckChip, + activeWeekDays = activeWeekDays, + isActiveCalendar = isActiveCalendar, + isActiveDial = isActiveDial, + calendarMonth = calendarMonth, + selectedDate = selectedDate, + selectedTime = selectedTime, + today = today, + isInitialPlacePicker = isInitialPlacePicker, + isHomeAddressInitialized = isHomeAddressInitialized, + placePickerState = placePickerState, + preparationAlarm = preparationAlarm, + departureAlarm = departureAlarm, + scheduleTitle = scheduleTitle, + showBottomSheet = showBottomSheet, + ) diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleViewModel.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleViewModel.kt new file mode 100644 index 00000000..80b99a6e --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/contract/GeneralScheduleViewModel.kt @@ -0,0 +1,498 @@ +package com.ondot.general.contract + +import androidx.lifecycle.viewModelScope +import com.dh.ondot.presentation.ui.theme.ERROR_CREATE_SCHEDULE +import com.dh.ondot.presentation.ui.theme.ERROR_GET_HOME_ADDRESS +import com.dh.ondot.presentation.ui.theme.ERROR_GET_PLACE_HISTORY +import com.dh.ondot.presentation.ui.theme.ERROR_GET_SCHEDULE_ALARMS +import com.dh.ondot.presentation.ui.theme.ERROR_SEARCH_PLACE +import com.ondot.domain.model.enums.RouterType +import com.ondot.domain.model.enums.ToastType +import com.ondot.domain.model.member.AddressInfo +import com.ondot.domain.model.member.HomeAddressInfo +import com.ondot.domain.model.member.PlaceHistory +import com.ondot.domain.model.request.CreateScheduleRequest +import com.ondot.domain.model.request.ScheduleAlarmRequest +import com.ondot.domain.model.schedule.ScheduleAlarm +import com.ondot.domain.repository.MemberRepository +import com.ondot.domain.repository.PlaceRepository +import com.ondot.domain.repository.ScheduleRepository +import com.ondot.ui.base.mvi.BaseViewModel +import com.ondot.util.DateTimeFormatter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime +import kotlin.time.ExperimentalTime + +@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) +class GeneralScheduleViewModel( + private val scheduleRepository: ScheduleRepository, + private val placeRepository: PlaceRepository, + private val memberRepository: MemberRepository, +) : BaseViewModel(GeneralScheduleState()) { + private val fullWeek = (0..6).toList() + private val weekDays = (1..5).toList() + private val weekend = listOf(0, 6) + private val query = MutableStateFlow("") + private var searchPlaceJob: Job? = null + + init { + viewModelScope.launch { + query + .debounce(100) + .distinctUntilChanged() + .onEach { value -> + if (value.isBlank()) { + searchPlaceJob?.cancel() + reduce { + copy( + placePickerState = placePickerState.copy(placeList = emptyList()), + ) + } + } + }.filter { it.isNotBlank() } + .collect { q -> + searchPlace(q) + } + } + } + + override suspend fun handleIntent(intent: GeneralScheduleIntent) { + when (intent) { + GeneralScheduleIntent.InitStep -> initStep() + is GeneralScheduleIntent.ToggleRepeat -> toggleRepeat(intent.isRepeat) + is GeneralScheduleIntent.SelectRepeatPreset -> selectRepeatPreset(intent.index) + is GeneralScheduleIntent.ToggleWeekDay -> toggleWeekDay(intent.index) + GeneralScheduleIntent.ToggleCalendar -> toggleCalendar() + GeneralScheduleIntent.MoveToPreviousMonth -> moveToPreviousMonth() + GeneralScheduleIntent.MoveToNextMonth -> moveToNextMonth() + is GeneralScheduleIntent.SelectDate -> selectDate(intent.date) + GeneralScheduleIntent.ToggleTimeDial -> toggleTimeDial() + is GeneralScheduleIntent.SelectTime -> selectTime(intent.time) + GeneralScheduleIntent.InitHomeAddress -> fetchHomeAddress() + GeneralScheduleIntent.InitPlaceHistory -> fetchPlaceHistory() + is GeneralScheduleIntent.SetFocusedRouterType -> setFocusedRouterType(intent.type) + is GeneralScheduleIntent.UpdateRouteInput -> updateRouteInput(intent.input) + is GeneralScheduleIntent.SelectPlace -> selectPlace(intent.place) + is GeneralScheduleIntent.SelectHistory -> selectHistory(intent.history) + is GeneralScheduleIntent.DeleteHistory -> deleteHistory(intent.history) + GeneralScheduleIntent.ToggleHomeDeparture -> toggleHomeDeparture() + is GeneralScheduleIntent.SetInitialPlacePicker -> setInitialPlacePicker(intent.isInitial) + GeneralScheduleIntent.ClickNext -> clickNext() + GeneralScheduleIntent.ClickBack -> clickBack() + is GeneralScheduleIntent.UpdateScheduleTitle -> updateScheduleTitle(intent.title) + GeneralScheduleIntent.TogglePreparationAlarm -> togglePreparationAlarm() + is GeneralScheduleIntent.SetBottomSheetVisible -> setBottomSheetVisible(intent.visible) + is GeneralScheduleIntent.CreateSchedule -> createSchedule(intent.isMedicationRequired, intent.preparationNote) + } + } + + private fun initStep() { + reduce { copy(totalStep = 2, currentStep = 1) } + } + + private fun toggleRepeat(newValue: Boolean) { + reduce { + copy( + isRepeat = newValue, + selectedDate = null, + activeCheckChip = if (newValue) activeCheckChip else null, + activeWeekDays = if (newValue) activeWeekDays else emptySet(), + ) + } + } + + private fun selectRepeatPreset(index: Int) { + reduce { + copy( + activeCheckChip = index, + isActiveCalendar = true, + activeWeekDays = + when (index) { + 0 -> fullWeek.toSet() + 1 -> weekDays.toSet() + 2 -> weekend.toSet() + else -> emptySet() + }, + ) + } + } + + private fun toggleWeekDay(index: Int) { + val nextActiveWeekDays = + currentState.activeWeekDays + .toMutableSet() + .apply { + if (contains(index)) remove(index) else add(index) + }.toSet() + + reduce { + copy( + isActiveCalendar = true, + activeWeekDays = nextActiveWeekDays, + activeCheckChip = + when (nextActiveWeekDays) { + fullWeek.toSet() -> 0 + weekDays.toSet() -> 1 + weekend.toSet() -> 2 + else -> null + }, + ) + } + } + + private fun toggleCalendar() { + reduce { copy(isActiveCalendar = !isActiveCalendar) } + } + + private fun moveToPreviousMonth() { + reduce { copy(calendarMonth = calendarMonth.minus(DatePeriod(months = 1))) } + } + + private fun moveToNextMonth() { + reduce { copy(calendarMonth = calendarMonth.plus(DatePeriod(months = 1))) } + } + + private fun selectDate(date: LocalDate) { + reduce { copy(selectedDate = date, isActiveDial = true) } + } + + private fun toggleTimeDial() { + reduce { copy(isActiveDial = !isActiveDial) } + } + + private fun selectTime(time: LocalTime) { + reduce { copy(selectedTime = time) } + } + + private fun searchPlace(query: String) { + searchPlaceJob?.cancel() + searchPlaceJob = + launchResult( + block = { placeRepository.searchPlaceAppResult(query) }, + onSuccess = { places -> + reduce { + copy( + placePickerState = placePickerState.copy(placeList = places), + ) + } + }, + onError = { + emitEffect(GeneralScheduleSideEffect.ShowToast(ERROR_SEARCH_PLACE, ToastType.ERROR)) + }, + ) + } + + private fun fetchHomeAddress() { + launchResult( + block = { memberRepository.fetchHomeAddress() }, + onSuccess = ::onSuccessGetHomeAddress, + onError = { + emitEffect(GeneralScheduleSideEffect.ShowToast(ERROR_GET_HOME_ADDRESS, ToastType.ERROR)) + }, + ) + } + + private fun onSuccessGetHomeAddress(result: HomeAddressInfo) { + reduce { + copy( + placePickerState = + placePickerState.copy( + homeAddress = + AddressInfo( + title = result.roadAddress, + roadAddress = result.roadAddress, + latitude = result.latitude, + longitude = result.longitude, + ), + ), + isHomeAddressInitialized = true, + ) + } + } + + private fun setFocusedRouterType(type: RouterType) { + reduce { copy(placePickerState = placePickerState.copy(lastFocusedTextField = type)) } + } + + private fun updateRouteInput(value: String) { + when (currentState.placePickerState.lastFocusedTextField) { + RouterType.Departure -> + reduce { + copy( + placePickerState = + placePickerState.copy( + isChecked = placePickerState.isChecked && value.isHomeAddressInput(), + departurePlaceInput = value, + selectedDeparturePlace = null, + ), + ) + } + + RouterType.Arrival -> + reduce { + copy( + placePickerState = + placePickerState.copy( + arrivalPlaceInput = value, + selectedArrivalPlace = null, + ), + ) + } + } + + query.value = value + } + + private fun selectPlace(place: AddressInfo) { + savePlaceHistory(place) + + when (currentState.placePickerState.lastFocusedTextField) { + RouterType.Departure -> { + reduce { + copy( + placePickerState = + placePickerState.copy( + placeList = emptyList(), + isChecked = placePickerState.isChecked && place.isHomeAddress(), + departurePlaceInput = place.title, + selectedDeparturePlace = place, + ), + ) + } + tryEmitEffect(GeneralScheduleSideEffect.RequestArrivalFocus) + setFocusedRouterType(RouterType.Arrival) + query.value = "" + } + + RouterType.Arrival -> + reduce { + copy( + placePickerState = + placePickerState.copy( + placeList = emptyList(), + arrivalPlaceInput = place.title, + selectedArrivalPlace = place, + ), + ) + } + } + } + + private fun selectHistory(place: PlaceHistory) { + selectPlace( + AddressInfo( + title = place.title, + roadAddress = place.roadAddress, + latitude = place.latitude, + longitude = place.longitude, + ), + ) + } + + private fun toggleHomeDeparture() { + val curValue = currentState.placePickerState.isChecked + + if (!curValue && + currentState.placePickerState.homeAddress.title + .isBlank() + ) { + fetchHomeAddress() + return + } + + reduce { + copy( + placePickerState = + placePickerState.copy( + isChecked = !curValue, + departurePlaceInput = if (!curValue) placePickerState.homeAddress.roadAddress else "", + selectedDeparturePlace = if (!curValue) placePickerState.homeAddress else null, + ), + ) + } + } + + private fun String.isHomeAddressInput(): Boolean { + val homeAddress = currentState.placePickerState.homeAddress + return this == homeAddress.roadAddress || this == homeAddress.title + } + + private fun AddressInfo.isHomeAddress(): Boolean { + val homeAddress = currentState.placePickerState.homeAddress + return roadAddress == homeAddress.roadAddress && + latitude == homeAddress.latitude && + longitude == homeAddress.longitude + } + + private fun setInitialPlacePicker(value: Boolean) { + reduce { copy(isInitialPlacePicker = value) } + } + + private fun fetchPlaceHistory() { + launchResult( + block = { placeRepository.fetchHistory() }, + onSuccess = { result -> + reduce { copy(placePickerState = placePickerState.copy(placeHistory = result)) } + }, + onError = { + emitEffect(GeneralScheduleSideEffect.ShowToast(ERROR_GET_PLACE_HISTORY, ToastType.ERROR)) + }, + ) + } + + private fun savePlaceHistory(place: AddressInfo) { + launchResult( + block = { placeRepository.saveHistory(place) }, + onSuccess = { fetchPlaceHistory() }, + ) + } + + private fun deleteHistory(place: PlaceHistory) { + launchResult( + block = { placeRepository.deleteHistory(place.searchedAt) }, + onSuccess = { fetchPlaceHistory() }, + ) + } + + @OptIn(ExperimentalTime::class) + private fun fetchScheduleAlarms() { + val (dateRepeat, time, places) = validateScheduleInputs() + val (date, _) = dateRepeat + val (departurePlace, arrivalPlace) = places + + val today = + kotlin.time.Clock.System + .now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + val appointmentAt = DateTimeFormatter.formatIsoDateTime(date ?: today, time) + + launchResult( + block = { + scheduleRepository.fetchScheduleAlarms( + request = + ScheduleAlarmRequest( + appointmentAt = appointmentAt, + startLatitude = departurePlace.latitude, + startLongitude = departurePlace.longitude, + endLatitude = arrivalPlace.latitude, + endLongitude = arrivalPlace.longitude, + ), + ) + }, + onSuccess = ::onSuccessGetScheduleAlarms, + onError = { + emitEffect(GeneralScheduleSideEffect.ShowToast(ERROR_GET_SCHEDULE_ALARMS, ToastType.ERROR)) + }, + ) + } + + private fun onSuccessGetScheduleAlarms(result: ScheduleAlarm) { + reduce { + copy( + preparationAlarm = result.preparationAlarm, + departureAlarm = result.departureAlarm, + ) + } + } + + @OptIn(ExperimentalTime::class) + private suspend fun createSchedule( + isMedicationRequired: Boolean, + preparationNote: String, + ) { + val (dateRepeat, time, places) = validateScheduleInputs() + val (date, _) = dateRepeat + val (departurePlace, arrivalPlace) = places + + val today = + kotlin.time.Clock.System + .now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + val appointmentAt = DateTimeFormatter.formatIsoDateTime(date ?: today, time) + + val request = + CreateScheduleRequest( + title = currentState.scheduleTitle, + isRepeat = currentState.isRepeat, + repeatDays = currentState.activeWeekDays.map { it + 1 }, + isMedicationRequired = isMedicationRequired, + preparationNote = preparationNote, + departurePlace = departurePlace, + arrivalPlace = arrivalPlace, + appointmentAt = appointmentAt, + preparationAlarm = currentState.preparationAlarm, + departureAlarm = currentState.departureAlarm, + ) + + launchResult( + block = { scheduleRepository.createScheduleAppResult(request) }, + onSuccess = { + emitEffect(GeneralScheduleSideEffect.NavigateToMain) + }, + onError = { + emitEffect(GeneralScheduleSideEffect.ShowToast(ERROR_CREATE_SCHEDULE, ToastType.ERROR)) + }, + ) + } + + private fun updateScheduleTitle(title: String) { + reduce { copy(scheduleTitle = title) } + } + + private fun togglePreparationAlarm() { + reduce { copy(preparationAlarm = preparationAlarm.copy(enabled = !preparationAlarm.enabled)) } + } + + private fun setBottomSheetVisible(visible: Boolean) { + reduce { copy(showBottomSheet = visible) } + } + + private suspend fun clickNext() { + when (currentState.currentStep) { + 1 -> { + reduce { copy(currentStep = currentStep + 1) } + emitEffect(GeneralScheduleSideEffect.NavigateToPlacePicker) + } + + 2 -> { + fetchScheduleAlarms() + emitEffect(GeneralScheduleSideEffect.NavigateToRouteLoading) + } + } + } + + private fun clickBack() { + when (currentState.currentStep) { + 2 -> reduce { copy(currentStep = currentStep - 1) } + } + } + + private fun validateScheduleInputs(): Triple>, LocalTime, Pair> { + val time = requireNotNull(currentState.selectedTime) { "selectedTime가 null입니다." } + val from = requireNotNull(currentState.placePickerState.selectedDeparturePlace) { "selectedDeparturePlace가 null입니다." } + val to = requireNotNull(currentState.placePickerState.selectedArrivalPlace) { "selectedArrivalPlace가 null입니다." } + + val date = currentState.selectedDate + val repeatDays = currentState.activeWeekDays + + require(!(date == null && repeatDays.isEmpty())) { "date가 null이면 repeatDays는 비어 있으면 안 됩니다." } + + return Triple(date to repeatDays, time, from to to) + } +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/di/GeneralModule.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/di/GeneralModule.kt index e93a633a..a0e26394 100644 --- a/feature/general/src/commonMain/kotlin/com/ondot/general/di/GeneralModule.kt +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/di/GeneralModule.kt @@ -1,14 +1,18 @@ package com.ondot.general.di import com.ondot.general.GeneralScheduleViewModel +import com.ondot.general.navigation.GeneralScheduleMviNavGraph import com.ondot.general.navigation.GeneralScheduleNavGraph import com.ondot.navigation.base.NavGraphContributor import org.koin.core.module.dsl.viewModelOf import org.koin.core.qualifier.named import org.koin.dsl.module +import com.ondot.general.contract.GeneralScheduleViewModel as GeneralScheduleMviViewModel val generalModule = module { viewModelOf(::GeneralScheduleViewModel) + viewModelOf(::GeneralScheduleMviViewModel) single(named("general")) { GeneralScheduleNavGraph } + single(named("generalMvi")) { GeneralScheduleMviNavGraph } } diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/navigation/GeneralScheduleMviNavGraph.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/navigation/GeneralScheduleMviNavGraph.kt new file mode 100644 index 00000000..7cbb3219 --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/navigation/GeneralScheduleMviNavGraph.kt @@ -0,0 +1,102 @@ +package com.ondot.general.navigation + +import androidx.compose.runtime.remember +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.ondot.general.contract.GeneralScheduleViewModel +import com.ondot.general.ui.check.CheckScheduleRoute +import com.ondot.general.ui.place.PlacePickerRoute +import com.ondot.general.ui.repeat.ScheduleRepeatSettingRoute +import com.ondot.navigation.NavRoutes +import com.ondot.navigation.base.NavGraphContributor +import com.ondot.ui.screen.loading.RouteLoadingScreen +import org.koin.compose.viewmodel.koinViewModel + +object GeneralScheduleMviNavGraph : NavGraphContributor { + override val graphRoute: NavRoutes + get() = NavRoutes.GeneralScheduleMviGraph + override val startDestination: String + get() = NavRoutes.ScheduleRepeatSettingMvi.route + + override fun NavGraphBuilder.registerGraph(navController: NavHostController) { + navigation( + route = graphRoute.route, + startDestination = startDestination, + ) { + composable(NavRoutes.ScheduleRepeatSettingMvi.route) { backStackEntry -> + val parentEntry = + remember(backStackEntry) { + navController.getBackStackEntry(graphRoute.route) + } + val viewModel: GeneralScheduleViewModel = koinViewModel(viewModelStoreOwner = parentEntry) + + ScheduleRepeatSettingRoute( + viewModel = viewModel, + navigateToMain = { + navController.navigate(NavRoutes.Main.route) { + popUpTo(graphRoute.route) { inclusive = true } + launchSingleTop = true + } + }, + navigateToPlacePicker = { + navController.navigate(NavRoutes.PlacePickerMvi.route) { + launchSingleTop = true + } + }, + ) + } + + composable(NavRoutes.PlacePickerMvi.route) { backStackEntry -> + val parentEntry = + remember(backStackEntry) { + navController.getBackStackEntry(graphRoute.route) + } + val viewModel: GeneralScheduleViewModel = koinViewModel(viewModelStoreOwner = parentEntry) + + PlacePickerRoute( + viewModel = viewModel, + popScreen = { navController.popBackStack() }, + navigateToRouteLoading = { + navController.navigate(NavRoutes.RouteLoadingMvi.route) { + launchSingleTop = true + } + }, + ) + } + + composable(NavRoutes.RouteLoadingMvi.route) { + RouteLoadingScreen( + navigateToNext = { + navController.navigate(NavRoutes.CheckScheduleMvi.route) { + popUpTo(NavRoutes.PlacePickerMvi.route) { + inclusive = false + } + launchSingleTop = true + } + }, + ) + } + + composable(NavRoutes.CheckScheduleMvi.route) { backStackEntry -> + val parentEntry = + remember(backStackEntry) { + navController.getBackStackEntry(graphRoute.route) + } + val viewModel: GeneralScheduleViewModel = koinViewModel(viewModelStoreOwner = parentEntry) + + CheckScheduleRoute( + viewModel = viewModel, + popScreen = { navController.popBackStack() }, + navigateToMain = { + navController.navigate(NavRoutes.Main.route) { + popUpTo(graphRoute.route) { inclusive = true } + launchSingleTop = true + } + }, + ) + } + } + } +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/repeat/ScheduleRepeatSettingScreen.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/repeat/ScheduleRepeatSettingScreen.kt index 618e05af..0169f81e 100644 --- a/feature/general/src/commonMain/kotlin/com/ondot/general/repeat/ScheduleRepeatSettingScreen.kt +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/repeat/ScheduleRepeatSettingScreen.kt @@ -2,7 +2,6 @@ package com.ondot.general.repeat import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -14,7 +13,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -46,7 +44,6 @@ fun ScheduleRepeatSettingScreen( navigateToPlacePicker: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val interactionSource = remember { MutableInteractionSource() } val scrollState = rememberScrollState() LaunchedEffect(uiState.totalStep) { @@ -71,7 +68,6 @@ fun ScheduleRepeatSettingScreen( ScheduleRepeatSettingContent( uiState = uiState, - interactionSource = interactionSource, scrollState = scrollState, isButtonEnabled = viewModel.isButtonEnabled(), onClickSwitch = viewModel::onClickSwitch, @@ -91,7 +87,6 @@ fun ScheduleRepeatSettingScreen( @Composable fun ScheduleRepeatSettingContent( uiState: GeneralScheduleUiState, - interactionSource: MutableInteractionSource, scrollState: ScrollState, isButtonEnabled: Boolean = false, onClickSwitch: (Boolean) -> Unit, @@ -160,7 +155,6 @@ fun ScheduleRepeatSettingContent( today = uiState.today, selectedTime = uiState.selectedTime, isActiveDial = uiState.isActiveDial, - interactionSource = interactionSource, onToggleCalendar = onToggleCalendar, onToggleDial = onToggleDial, onPrevMonth = onPrevMonth, diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/ui/check/CheckScheduleRoute.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/check/CheckScheduleRoute.kt new file mode 100644 index 00000000..2e46b08e --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/check/CheckScheduleRoute.kt @@ -0,0 +1,46 @@ +package com.ondot.general.ui.check + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.ondot.general.check.CheckScheduleContent +import com.ondot.general.contract.GeneralScheduleIntent +import com.ondot.general.contract.GeneralScheduleSideEffect +import com.ondot.general.contract.GeneralScheduleViewModel +import com.ondot.general.contract.toLegacyUiState +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun CheckScheduleRoute( + viewModel: GeneralScheduleViewModel = koinViewModel(), + popScreen: () -> Unit, + navigateToMain: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val focusRequest = remember { FocusRequester() } + + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is GeneralScheduleSideEffect.NavigateToMain -> navigateToMain() + else -> Unit + } + } + } + + CheckScheduleContent( + uiState = uiState.toLegacyUiState(), + focusRequester = focusRequest, + onClickBack = popScreen, + onCreateSchedule = { isMedicationRequired, preparationNote -> + viewModel.dispatch(GeneralScheduleIntent.CreateSchedule(isMedicationRequired, preparationNote)) + }, + onValueChanged = { viewModel.dispatch(GeneralScheduleIntent.UpdateScheduleTitle(it)) }, + onToggleSwitch = { viewModel.dispatch(GeneralScheduleIntent.TogglePreparationAlarm) }, + onShowBottomSheet = { viewModel.dispatch(GeneralScheduleIntent.SetBottomSheetVisible(true)) }, + onDismiss = { viewModel.dispatch(GeneralScheduleIntent.SetBottomSheetVisible(false)) }, + ) +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/ui/place/PlacePickerRoute.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/place/PlacePickerRoute.kt new file mode 100644 index 00000000..1911f8bf --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/place/PlacePickerRoute.kt @@ -0,0 +1,88 @@ +package com.ondot.general.ui.place + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.ondot.general.contract.GeneralScheduleIntent +import com.ondot.general.contract.GeneralScheduleSideEffect +import com.ondot.general.contract.GeneralScheduleViewModel +import com.ondot.platform.util.BackPressHandler +import com.ondot.ui.screen.placepicker.PlacePickerScreen +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun PlacePickerRoute( + viewModel: GeneralScheduleViewModel = koinViewModel(), + popScreen: () -> Unit, + navigateToRouteLoading: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val departureFocusRequester = remember { FocusRequester() } + val arrivalFocusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + LaunchedEffect(Unit) { + if (uiState.placePickerState.homeAddress.title + .isBlank() && + !uiState.isHomeAddressInitialized + ) { + viewModel.dispatch(GeneralScheduleIntent.InitHomeAddress) + } + } + + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + GeneralScheduleSideEffect.NavigateToRouteLoading -> navigateToRouteLoading() + GeneralScheduleSideEffect.RequestArrivalFocus -> arrivalFocusRequester.requestFocus() + else -> Unit + } + } + } + + LaunchedEffect(uiState.isInitialPlacePicker) { + if (uiState.isInitialPlacePicker) { + departureFocusRequester.requestFocus() + viewModel.dispatch(GeneralScheduleIntent.SetInitialPlacePicker(false)) + } + } + + LaunchedEffect(Unit) { + viewModel.dispatch(GeneralScheduleIntent.InitPlaceHistory) + } + + BackPressHandler( + onBack = { + viewModel.dispatch(GeneralScheduleIntent.ClickBack) + popScreen() + }, + ) + + PlacePickerScreen( + state = uiState.placePickerState, + buttonEnabled = uiState.isPlacePickerButtonEnabled, + departureFocusRequester = departureFocusRequester, + arrivalFocusRequester = arrivalFocusRequester, + onRouteInputChanged = { viewModel.dispatch(GeneralScheduleIntent.UpdateRouteInput(it)) }, + onRouteInputFocused = { viewModel.dispatch(GeneralScheduleIntent.SetFocusedRouterType(it)) }, + onPlaceSelected = { + viewModel.dispatch(GeneralScheduleIntent.SelectPlace(it)) + focusManager.clearFocus() + }, + onHistorySelected = { + viewModel.dispatch(GeneralScheduleIntent.SelectHistory(it)) + focusManager.clearFocus() + }, + onDeleteHistory = { viewModel.dispatch(GeneralScheduleIntent.DeleteHistory(it)) }, + onToggleCheckBox = { viewModel.dispatch(GeneralScheduleIntent.ToggleHomeDeparture) }, + onNext = { viewModel.dispatch(GeneralScheduleIntent.ClickNext) }, + popScreen = { + viewModel.dispatch(GeneralScheduleIntent.ClickBack) + popScreen() + }, + ) +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/ui/repeat/ScheduleRepeatSettingRoute.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/repeat/ScheduleRepeatSettingRoute.kt new file mode 100644 index 00000000..35f99081 --- /dev/null +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/ui/repeat/ScheduleRepeatSettingRoute.kt @@ -0,0 +1,61 @@ +package com.ondot.general.ui.repeat + +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.ondot.general.contract.GeneralScheduleIntent +import com.ondot.general.contract.GeneralScheduleSideEffect +import com.ondot.general.contract.GeneralScheduleViewModel +import com.ondot.general.contract.toLegacyUiState +import com.ondot.general.repeat.ScheduleRepeatSettingContent +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun ScheduleRepeatSettingRoute( + viewModel: GeneralScheduleViewModel = koinViewModel(), + navigateToMain: () -> Unit, + navigateToPlacePicker: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val scrollState = rememberScrollState() + + LaunchedEffect(uiState.totalStep) { + if (uiState.totalStep == 0) { + viewModel.dispatch(GeneralScheduleIntent.InitStep) + } + } + + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + GeneralScheduleSideEffect.NavigateToPlacePicker -> navigateToPlacePicker() + else -> Unit + } + } + } + + LaunchedEffect(uiState.isActiveDial) { + if (uiState.isActiveDial) { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + + ScheduleRepeatSettingContent( + uiState = uiState.toLegacyUiState(), + scrollState = scrollState, + isButtonEnabled = uiState.isRepeatStepButtonEnabled, + onClickSwitch = { viewModel.dispatch(GeneralScheduleIntent.ToggleRepeat(it)) }, + onClickCheckTextChip = { viewModel.dispatch(GeneralScheduleIntent.SelectRepeatPreset(it)) }, + onClickTextChip = { viewModel.dispatch(GeneralScheduleIntent.ToggleWeekDay(it)) }, + onToggleCalendar = { viewModel.dispatch(GeneralScheduleIntent.ToggleCalendar) }, + onToggleDial = { viewModel.dispatch(GeneralScheduleIntent.ToggleTimeDial) }, + onPrevMonth = { viewModel.dispatch(GeneralScheduleIntent.MoveToPreviousMonth) }, + onNextMonth = { viewModel.dispatch(GeneralScheduleIntent.MoveToNextMonth) }, + onDateSelected = { viewModel.dispatch(GeneralScheduleIntent.SelectDate(it)) }, + onTimeSelected = { viewModel.dispatch(GeneralScheduleIntent.SelectTime(it)) }, + navigateToMain = navigateToMain, + onClickButton = { viewModel.dispatch(GeneralScheduleIntent.ClickNext) }, + ) +} diff --git a/feature/main/src/commonMain/kotlin/com/ondot/main/navigation/MainNavGraph.kt b/feature/main/src/commonMain/kotlin/com/ondot/main/navigation/MainNavGraph.kt index 723903d5..303fd3be 100644 --- a/feature/main/src/commonMain/kotlin/com/ondot/main/navigation/MainNavGraph.kt +++ b/feature/main/src/commonMain/kotlin/com/ondot/main/navigation/MainNavGraph.kt @@ -22,7 +22,7 @@ object MainNavGraph : NavGraphContributor { composable(NavRoutes.Main.route) { MainScreen( navigateToGeneralSchedule = { - navController.navigate(NavRoutes.GeneralScheduleGraph.route) { + navController.navigate(NavRoutes.GeneralScheduleMviGraph.route) { launchSingleTop = true } },