From 7f889337d96d3555acb9be4666b923afe28117e9 Mon Sep 17 00:00:00 2001 From: Dell Date: Sat, 29 Nov 2025 21:31:31 +0300 Subject: [PATCH] our commit message --- .../work/ui/screen/NavigationGraph.kt | 18 +- .../work/ui/screen/auth/AuthScreen.kt | 1 - .../work/ui/screen/book/BookScreen.kt | 166 +++++++++++++- .../work/ui/screen/book/BookingViewModel.kt | 87 ++++++++ .../work/ui/screen/main/MainScreen.kt | 208 +++++++++++++++++- 5 files changed, 458 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt 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 7890fab..5577ca7 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 @@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -34,10 +31,21 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - MainScreen(navController = navController) + MainScreen( + navController = navController, + onNavigateToBooking = { + navController.navigate(BookScreenDestination) + } + ) } composable { - BookScreen(navController = navController) + BookScreen( + onBack = { navController.popBackStack() }, + onBookingSuccess = { + // Возвращаемся на главный экран и обновляем его + navController.popBackStack() + } + ) } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index a8aac84..c9f4d5f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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 b225888..cdbcfb4 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,16 +1,168 @@ package ru.myitschool.work.ui.screen.book -import androidx.compose.foundation.layout.Box import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.navigation.NavController +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.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.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +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 @Composable -fun BookScreen(navController: NavController){ - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") +fun BookingScreen( + uiState: BookingUiState, // состояние интерфейса + onSelectDate: (LocalDate) -> Unit, // callback при выборе даты + onSelectPlace: (String) -> Unit, // callback при выборе места + onBook: () -> Unit, // callback при бронировании + onBack: () -> Unit, // callback при нажатии "Назад" + onRefresh: () -> Unit // callback при обновлении +) { + // Сортировка дат по порядку + val sortedDates = uiState.dates.sorted() + // Фильтрация дат, для которых есть доступные места + val availableDates = sortedDates.filter { date -> uiState.places[date]?.isNotEmpty() == true } + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + // Вкладки для выбора дат + if (availableDates.isNotEmpty()) { + ScrollableTabRow(selectedTabIndex = availableDates.indexOf(uiState.selectedDate)) { + availableDates.forEachIndexed { index, date -> + Tab( + selected = date == uiState.selectedDate, + onClick = { onSelectDate(date) }, + text = { + Text( + text = date.format(DateTimeFormatter.ofPattern("dd.MM")), + modifier = Modifier.testTag("book_date_pos_$index") + ) + }, + modifier = Modifier.testTag("book_date") + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Список мест для выбранной даты + val placesForDate = uiState.selectedDate?.let { uiState.places[it] } ?: emptyList() + + if (placesForDate.isNotEmpty()) { + Column { + placesForDate.forEachIndexed { index, place -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .selectable( + selected = uiState.selectedPlace == place, + onClick = { onSelectPlace(place) } + ) + .testTag("book_place_pos_$index"), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = place, + modifier = Modifier.weight(1f).testTag("book_place_text") + ) + RadioButton( + selected = uiState.selectedPlace == place, + onClick = { onSelectPlace(place) }, + modifier = Modifier.testTag("book_place_selector") + ) + } + } + } + } + + // пустой список (все забронировано) + if (availableDates.isEmpty() && !uiState.isError) { + Text( + text = "Всё забронировано", + modifier = Modifier.testTag("book_empty") + ) + } + + // ошибка + if (uiState.isError) { + Text( + text = uiState.errorMessage ?: "Ошибка загрузки", + color = Color.Red, + modifier = Modifier.testTag("book_error") + ) + + Button( + onClick = onRefresh, + modifier = Modifier.testTag("book_refresh_button") + ) { + Text("Обновить") + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // Кнопки: Забронировать и Назад + if (!uiState.isError && placesForDate.isNotEmpty()) { + Button( + onClick = onBook, + enabled = uiState.selectedPlace != null, // активна только при выбранном месте + modifier = Modifier.fillMaxWidth().testTag("book_book_button") + ) { Text("Забронировать") } + } + + Button( + onClick = onBack, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag("book_back_button") + ) { + Text("Назад") + } } +} + +// Модель состояния интерфейса +data class BookingUiState( + val dates: List = emptyList(), // список доступных дат + val places: Map> = emptyMap(), // места по датам + val selectedDate: LocalDate? = null, // выбранная дата + val selectedPlace: String? = null, // выбранное место + val isError: Boolean = false, // флаг ошибки + val errorMessage: String? = null // сообщение об ошибке +) + +@Composable +fun BookScreen( + onBack: () -> Unit, // callback при возврате назад + onBookingSuccess: () -> Unit // callback при успешном бронировании +) { + val viewModel: BookingViewModel = viewModel() + 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/BookingViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt new file mode 100644 index 0000000..928017d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt @@ -0,0 +1,87 @@ +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(BookingUiState()) + 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 diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index c9d1101..f931280 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -1,17 +1,207 @@ package ru.myitschool.work.ui.screen.main -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* +import ru.myitschool.work.ui.nav.BookScreenDestination + +// Модель данных для бронирования +data class BookingItem( + val date: String, // Формат "dd.MM.yyyy" + val place: String, + val id: Int +) @Composable -fun MainScreen(navController: NavController) { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") +fun MainScreen( + navController: NavController, + onNavigateToBooking: () -> Unit +) { + // Состояния + var userName by remember { mutableStateOf("Иван Иванов") } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var bookingItems by remember { mutableStateOf(emptyList()) } + var hasError by remember { mutableStateOf(false) } + + // Для корутин + val coroutineScope = rememberCoroutineScope() + + // Функция загрузки данных + fun loadData() { + isLoading = true + hasError = false + + coroutineScope.launch { + kotlinx.coroutines.delay(1000) // Имитация задержки + + // Имитация ответа от сервера + val response = listOf( + BookingItem("20.12.2023", "Конференц-зал А", 1), + BookingItem("15.12.2023", "Переговорная Б", 2), + BookingItem("25.12.2023", "Спортзал", 3) + ) + + // Сортировка по дате (увеличение) + bookingItems = response.sortedBy { + SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).parse(it.date) + } + + isLoading = false + } + } + + // Первая загрузка при открытии экрана + LaunchedEffect(Unit) { + loadData() + } + + // Если ошибка - показываем только ошибку и кнопку обновления + if (hasError) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Текстовое поле с ошибкой (main_error) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопка обновления (main_refresh_button) + Button(onClick = { loadData() }) { + Text("Обновить") + } + } + } else { + // Нормальное состояние + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // Верхняя строка + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Фото пользователя (main_photo) + Image( + painter = painterResource(id = android.R.drawable.ic_menu_gallery), + contentDescription = "Фото", + modifier = Modifier.size(64.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + // Имя пользователя (main_name) + Text( + text = userName, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + + // Кнопка выхода (main_logout_button) + Button(onClick = { + // Очистка данных и переход на авторизацию + userName = "" + bookingItems = emptyList() + navController.navigate("auth") { popUpTo(0) } + }) { + Text("Выход") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопки действий + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Кнопка обновления (main_refresh_button) + Button( + onClick = { loadData() }, + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Обновить") + } + } + + Button( + onClick = { navController.navigate(BookScreenDestination) } + ) { + Text("Перейти к бронированию") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Список бронирований + if (bookingItems.isNotEmpty()) { + LazyColumn(modifier = Modifier.weight(1f)) { + items(bookingItems) { item -> + // Элемент списка (main_book_pos_{index}) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + // Дата бронирования (main_item_date) + Text( + text = "Дата: ${item.date}", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Место бронирования (main_item_place) + Text( + text = "Место: ${item.place}", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } else { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Нет бронирований", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + } } } -