diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 93e99c6..ae03202 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -38,7 +38,7 @@ fun AppNavHost( composable { BookScreen( onBack = { navController.popBackStack() }, - onBookingSuccess = { + onBookSuccess = { // Возвращаемся на главный экран и обновляем его navController.popBackStack() } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt new file mode 100644 index 0000000..2bd500d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookAction { + data class ShowError(val message: String?) : BookAction + object BookSuccess : BookAction +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt new file mode 100644 index 0000000..4630d54 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.ui.screen.book + +import java.time.LocalDate + +sealed interface BookIntent { + object LoadData : BookIntent + object Refresh : BookIntent + object BookPlace : BookIntent + data class SelectDate(val date: LocalDate) : BookIntent + data class SelectPlace(val place: String) : BookIntent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 0953e8b..25213fb 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -1,38 +1,74 @@ package ru.myitschool.work.ui.screen.book -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable -import androidx.compose.material3.Button -import androidx.compose.material3.RadioButton -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import java.time.LocalDate import java.time.format.DateTimeFormatter -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.lifecycle.viewmodel.compose.viewModel +import ru.myitschool.work.core.TestIds @Composable -fun BookingScreen( - uiState: BookingState, // состояние интерфейса - onSelectDate: (LocalDate) -> Unit, // callback при выборе даты - onSelectPlace: (String) -> Unit, // callback при выборе места - onBook: () -> Unit, // callback при бронировании - onBack: () -> Unit, // callback при нажатии "Назад" - onRefresh: () -> Unit // callback при обновлении +fun BookScreen( + onBack: () -> Unit, + onBookSuccess: () -> Unit +) { + val viewModel: BookViewModel = viewModel() + val uiState by viewModel.uiState.collectAsState() + + // Обработка действий + val event = viewModel.actionFlow.collectAsState(initial = null) + LaunchedEffect(event.value) { + when (event.value) { + is BookAction.BookSuccess -> { + onBookSuccess() + } + else -> {} + } + } + + // Загрузка начальных данных + LaunchedEffect(Unit) { + viewModel.onIntent(BookIntent.LoadData) + } + + when (uiState) { + is BookState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is BookState.Data -> { + BookContentScreen( + uiState = uiState as BookState.Data, + onSelectDate = { date -> viewModel.onIntent(BookIntent.SelectDate(date)) }, + onSelectPlace = { place -> viewModel.onIntent(BookIntent.SelectPlace(place)) }, + onBook = { viewModel.onIntent(BookIntent.BookPlace) }, + onBack = onBack, + onRefresh = { viewModel.onIntent(BookIntent.Refresh) } + ) + } + } +} + +@Composable +fun BookContentScreen( + uiState: BookState.Data, + onSelectDate: (LocalDate) -> Unit, + onSelectPlace: (String) -> Unit, + onBook: () -> Unit, + onBack: () -> Unit, + onRefresh: () -> Unit ) { // Сортировка дат по порядку val sortedDates = uiState.dates.sorted() @@ -42,7 +78,9 @@ fun BookingScreen( Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // Вкладки для выбора дат if (availableDates.isNotEmpty()) { - ScrollableTabRow(selectedTabIndex = availableDates.indexOf(uiState.selectedDate)) { + ScrollableTabRow( + selectedTabIndex = availableDates.indexOf(uiState.selectedDate), + ) { availableDates.forEachIndexed { index, date -> Tab( selected = date == uiState.selectedDate, @@ -50,10 +88,10 @@ fun BookingScreen( text = { Text( text = date.format(DateTimeFormatter.ofPattern("dd.MM")), - modifier = Modifier.testTag("book_date_pos_$index") + modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) ) }, - modifier = Modifier.testTag("book_date") + modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) ) } } @@ -75,17 +113,17 @@ fun BookingScreen( selected = uiState.selectedPlace == place, onClick = { onSelectPlace(place) } ) - .testTag("book_place_pos_$index"), + .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), verticalAlignment = Alignment.CenterVertically ) { Text( text = place, - modifier = Modifier.weight(1f).testTag("book_place_text") + modifier = Modifier.weight(1f).testTag(TestIds.Book.ITEM_PLACE_TEXT) ) RadioButton( selected = uiState.selectedPlace == place, onClick = { onSelectPlace(place) }, - modifier = Modifier.testTag("book_place_selector") + modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR) ) } } @@ -96,7 +134,7 @@ fun BookingScreen( if (availableDates.isEmpty() && !uiState.isError) { Text( text = "Всё забронировано", - modifier = Modifier.testTag("book_empty") + modifier = Modifier.testTag(TestIds.Book.EMPTY) ) } @@ -105,12 +143,12 @@ fun BookingScreen( Text( text = uiState.errorMessage ?: "Ошибка загрузки", color = Color.Red, - modifier = Modifier.testTag("book_error") + modifier = Modifier.testTag(TestIds.Book.ERROR) ) Button( onClick = onRefresh, - modifier = Modifier.testTag("book_refresh_button") + modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON) ) { Text("Обновить") } @@ -123,39 +161,15 @@ fun BookingScreen( Button( onClick = onBook, enabled = uiState.selectedPlace != null, // активна только при выбранном месте - modifier = Modifier.fillMaxWidth().testTag("book_book_button") + modifier = Modifier.fillMaxWidth().testTag(TestIds.Book.BOOK_BUTTON) ) { Text("Забронировать") } } Button( onClick = onBack, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag("book_back_button") + modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag(TestIds.Book.BACK_BUTTON) ) { Text("Назад") } } -} - - - - -@Composable -fun BookScreen( - onBack: () -> Unit, // callback при возврате назад - onBookingSuccess: () -> Unit // callback при успешном бронировании -) { - val viewModel: BookingViewModel = BookingViewModel() - val uiState by viewModel.uiState.collectAsState() - - BookingScreen( - uiState = uiState, - onSelectDate = { date -> viewModel.selectDate(date) }, - onSelectPlace = { place -> viewModel.selectPlace(place) }, - onBook = { - viewModel.bookPlace() - onBookingSuccess() - }, - onBack = onBack, - onRefresh = { viewModel.refresh() } - ) } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt new file mode 100644 index 0000000..5677a72 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.ui.screen.book + +import java.time.LocalDate + +sealed interface BookState { + object Loading : BookState + data class Data( + val dates: List = emptyList(), + val places: Map> = emptyMap(), + val selectedDate: LocalDate? = null, + val selectedPlace: String? = null, + val isError: Boolean = false, + val errorMessage: String? = null + ) : BookState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt new file mode 100644 index 0000000..81ac5e2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,126 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.time.LocalDate + +class BookViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(BookState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + init { + loadBookData() + } + + fun onIntent(intent: BookIntent) { + when (intent) { + is BookIntent.LoadData -> loadBookData() + is BookIntent.Refresh -> refresh() + is BookIntent.BookPlace -> bookPlace() + is BookIntent.SelectDate -> selectDate(intent.date) + is BookIntent.SelectPlace -> selectPlace(intent.place) + } + } + + private fun loadBookData() { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { BookState.Loading } + + try { + // Временные mock данные + val mockDates = listOf( + LocalDate.now().plusDays(1), + LocalDate.now().plusDays(2), + LocalDate.now().plusDays(3) + ) + + val mockPlaces = mapOf( + mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), + mockDates[1] to listOf("Место 1", "Место 2"), + mockDates[2] to listOf("Место 1") + ) + + val sortedDates = mockDates.sorted() + val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } + val defaultDate = availableDates.firstOrNull() + + _uiState.update { + BookState.Data( + dates = sortedDates, + places = mockPlaces, + selectedDate = defaultDate, + selectedPlace = null, + isError = false, + errorMessage = null + ) + } + } catch (e: Exception) { + _uiState.update { + BookState.Data( + isError = true, + errorMessage = "Ошибка загрузки данных" + ) + } + _actionFlow.emit(BookAction.ShowError("Ошибка загрузки данных")) + } + } + } + + private fun selectDate(date: LocalDate) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy( + selectedDate = date, + selectedPlace = null + ) + else -> currentState + } + } + } + + private fun selectPlace(place: String) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy(selectedPlace = place) + else -> currentState + } + } + } + + private fun bookPlace() { + viewModelScope.launch(Dispatchers.IO) { + try { + // вызов API для бронирования + // временная имитация успеха + _actionFlow.emit(BookAction.BookSuccess) + } catch (e: Exception) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy( + isError = true, + errorMessage = "Ошибка бронирования" + ) + else -> currentState + } + } + _actionFlow.emit(BookAction.ShowError("Ошибка бронирования")) + } + } + } + + private fun refresh() { + loadBookData() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt deleted file mode 100644 index b61dcc7..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -import java.time.LocalDate - -data class BookingState( - val dates: List = emptyList(), // список доступных дат - val places: Map> = emptyMap(), // места по датам - val selectedDate: LocalDate? = null, // выбранная дата - val selectedPlace: String? = null, // выбранное место - val isError: Boolean = false, // флаг ошибки - val errorMessage: String? = null // сообщение об ошибке -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt deleted file mode 100644 index 96133c8..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import java.time.LocalDate - -class BookingViewModel : ViewModel() { - - private val _uiState = MutableStateFlow(BookingState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - loadBookingData() - } - - fun loadBookingData() { - viewModelScope.launch { - try { - // Временные mock данные - val mockDates = listOf( - LocalDate.now().plusDays(1), - LocalDate.now().plusDays(2), - LocalDate.now().plusDays(3) - ) - - val mockPlaces = mapOf( - mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), - mockDates[1] to listOf("Место 1", "Место 2"), - mockDates[2] to listOf("Место 1") - ) - - val sortedDates = mockDates.sorted() - val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } - val defaultDate = availableDates.firstOrNull() - - _uiState.value = _uiState.value.copy( - dates = sortedDates, - places = mockPlaces, - selectedDate = defaultDate, - selectedPlace = null, - isError = false, - errorMessage = null - ) - - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - isError = true, - errorMessage = "Ошибка загрузки данных" - ) - } - } - } - - fun selectDate(date: LocalDate) { - _uiState.value = _uiState.value.copy( - selectedDate = date, - selectedPlace = null - ) - } - - fun selectPlace(place: String) { - _uiState.value = _uiState.value.copy( - selectedPlace = place - ) - } - - fun bookPlace() { - viewModelScope.launch { - try { - //вызов API для бронирования - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - isError = true, - errorMessage = "Ошибка бронирования" - ) - } - } - } - - fun refresh() { - loadBookingData() - } -} \ No newline at end of file