From 9e6ef6062fe97e81a42449e9ab6983a089f36c5e Mon Sep 17 00:00:00 2001 From: githubchikov Date: Thu, 11 Dec 2025 22:14:19 +0300 Subject: [PATCH] feat: add query's to backend --- app/build.gradle.kts | 3 + .../work/data/source/NetworkDataSource.kt | 16 +- .../work/ui/screen/NavigationGraph.kt | 19 +- .../work/ui/screen/auth/AuthScreen.kt | 14 +- .../work/ui/screen/book/BookIntent.kt | 10 +- .../work/ui/screen/book/BookScreen.kt | 221 +++++++++++--- .../work/ui/screen/book/BookState.kt | 16 +- .../work/ui/screen/book/BookViewModel.kt | 148 +++++++-- .../work/ui/screen/main/MainScreen.kt | 287 +++++++++--------- .../work/ui/screen/main/MainState.kt | 4 +- 10 files changed, 513 insertions(+), 225 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b61370..57eaa73 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,9 @@ android { dependencies { implementation("androidx.compose.material3:material3:1.4.0") + implementation("androidx.compose.runtime:runtime:1.10.0") + implementation("androidx.compose.foundation:foundation-layout:1.10.0") + implementation("androidx.compose.foundation:foundation:1.10.0") defaultComposeLibrary() implementation("androidx.datastore:datastore-preferences:1.1.7") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 8772066..da6c74f 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -4,6 +4,7 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers @@ -28,8 +29,16 @@ object NetworkDataSource { } suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { + val url = getUrl(code, Constants.AUTH_URL) + + println("➡ Request URL: $url") + runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) + val response = client.get(url) + + println("⬅ Response status: ${response.status}") + println("⬅ Response body: ${response.bodyAsText()}") + when (response.status) { HttpStatusCode.OK -> true HttpStatusCode.Unauthorized -> error("Код не существует") @@ -38,8 +47,9 @@ object NetworkDataSource { } }.mapCatching { success -> success - }.recoverCatching { _ -> - throw Exception("Не удалось соединиться с сервером") + }.recoverCatching { e -> + println("❌ Error: ${e.message}") + throw Exception(e.message) } } 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 d843e79..34e7d6b 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 @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -16,6 +17,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.book.BookScreen +import ru.myitschool.work.ui.screen.book.BookViewModel import ru.myitschool.work.ui.screen.main.MainScreen @Composable @@ -28,8 +30,8 @@ fun AppNavHost( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, -// startDestination = AuthScreenDestination, - startDestination = MainScreenDestination, + startDestination = AuthScreenDestination, +// startDestination = MainScreenDestination, ) { composable { AuthScreen(navController = navController) @@ -38,7 +40,18 @@ fun AppNavHost( MainScreen(navController = navController) } composable { - BookScreen(navController = navController) + val vm: BookViewModel = viewModel() + + BookScreen( + vm = vm, + onBack = { navController.popBackStack() }, + onSuccess = { + navController.popBackStack() + navController.navigate(MainScreenDestination) { + launchSingleTop = true + } + } + ) } } } \ 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 065a0c2..4e5eaf1 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 @@ -121,12 +121,14 @@ private fun Content( enter = fadeIn(), exit = fadeOut() ) { - Text( - text = (state as AuthState.Error).message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) + (state as? AuthState.Error)?.let { errorState -> + Text( + text = errorState.message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } } Spacer(modifier = Modifier.size(16.dp)) 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 index ea49392..55a6a3c 100644 --- 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 @@ -1,6 +1,12 @@ package ru.myitschool.work.ui.screen.book +import java.time.LocalDate + sealed interface BookIntent { - data class Send(val text: String): BookIntent - data class TextInput(val text: String): BookIntent + object Load : BookIntent + data class SelectDate(val date: LocalDate) : BookIntent + data class SelectPlace(val placeId: String) : BookIntent + object ConfirmBooking : BookIntent + object Refresh : BookIntent + object Back : 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 6adc0ed..2043de2 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 @@ -5,67 +5,208 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.myitschool.work.ui.nav.MainScreenDestination +/* +Экран бронирования +На данном экране необходимо вывести возможные даты и места для бронирования. + +Элементы, которые должны присутствовать на экране: +Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. + +В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: + Текстовое поле (book_place_text), в котором содержится место доступное для брони. + Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) + +Кнопка (book_book_button) для бронирования. +Кнопка (book_back_button) для возвращения на предыдущий экран. +По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. +По умолчанию неотображаемая кнопка обновить (book_refresh_button). +По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + +Требования к компонентам: +По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). +Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. +Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. +В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. +При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. + */ + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.Composable +import androidx.compose.ui.draw.clip +import ru.myitschool.work.ui.screen.auth.AuthIntent +import java.time.format.DateTimeFormatter + @Composable fun BookScreen( - viewModel: BookViewModel = viewModel(), - navController: NavController + vm: BookViewModel, + onBack: () -> Unit, + onSuccess: () -> Unit ) { - val state by viewModel.uiState.collectAsState() + val state by vm.state.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + vm.accept(BookIntent.Load) + } + + LaunchedEffect(state.bookingSuccess) { + if (state.bookingSuccess) { + onSuccess() } } - /* - Экран бронирования - На данном экране необходимо вывести возможные даты и места для бронирования. - Элементы, которые должны присутствовать на экране: - Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. - В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: - Текстовое поле (book_place_text), в котором содержится место доступное для брони. - Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) - Кнопка (book_book_button) для бронирования. - Кнопка (book_back_button) для возвращения на предыдущий экран. - По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. - По умолчанию неотображаемая кнопка обновить (book_refresh_button). - По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + when { + state.loading -> { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } - Требования к компонентам: - По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). - Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. - Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. - В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. - При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. - */ - - Column( - modifier = Modifier - .fillMaxSize() - .padding(all = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - - Text( - text = "страница бронирования...", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center + state.error -> ErrorBlock( + onRefresh = { vm.accept(BookIntent.Refresh) }, + onBack = { onBack() } ) + state.isEmpty -> EmptyBlock( + onBack = { onBack() } + ) + + else -> ContentBlock( + state = state, + onDateClick = { vm.accept(BookIntent.SelectDate(it)) }, + onPlaceClick = { vm.accept(BookIntent.SelectPlace(it)) }, + onConfirm = { vm.accept(BookIntent.ConfirmBooking) }, + onBack = { onBack() } + ) + } +} + +@Composable +private fun ContentBlock( + state: BookState, + onDateClick: (java.time.LocalDate) -> Unit, + onPlaceClick: (String) -> Unit, + onConfirm: () -> Unit, + onBack: () -> Unit +) { + val f = DateTimeFormatter.ofPattern("dd.MM") + + Column(Modifier.fillMaxSize().padding(16.dp)) { + + PrimaryScrollableTabRow( + selectedTabIndex = state.dates.indexOfFirst { it.date == state.selectedDate } + ) { + state.dates.forEachIndexed { index, item -> + Tab( + selected = state.selectedDate == item.date, + onClick = { onDateClick(item.date) }, + text = { Text(item.date.format(f)) } + ) + } + } + + Spacer(Modifier.height(16.dp)) + + // группа единственного выбора + Column(Modifier.selectableGroup()) { + state.availablePlaces.forEach { place -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .selectable( + selected = state.selectedPlaceId == place.id, + onClick = { onPlaceClick(place.id) } + ) + .padding(horizontal = 8.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 0.dp), + text = place.title + ) + + RadioButton( + selected = state.selectedPlaceId == place.id, + onClick = { onPlaceClick(place.id) } + ) + } + } + } + + Spacer(Modifier.height(24.dp)) + + Button( + enabled = state.selectedPlaceId != null, + onClick = onConfirm, + modifier = Modifier.fillMaxWidth() + ) { + Text("Бронировать") + } + + Spacer(Modifier.height(12.dp)) + + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } + } +} + + +@Composable +fun ErrorBlock(onRefresh: () -> Unit, onBack: () -> Unit) { + Column( + Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center + ) { + Text("Ошибка загрузки", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(12.dp)) + Button(onClick = onRefresh, modifier = Modifier.fillMaxWidth()) { + Text("Обновить") + } + Spacer(Modifier.height(12.dp)) + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } + } +} + +@Composable +fun EmptyBlock(onBack: () -> Unit) { + Column( + Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center + ) { + Text("Всё забронировано", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(16.dp)) + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } } } \ 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 index 020007e..e0c3e9e 100644 --- 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 @@ -1,5 +1,17 @@ package ru.myitschool.work.ui.screen.book +import java.time.LocalDate -sealed interface BookState { - object Data: BookState +data class BookState( + val loading: Boolean = true, + val error: Boolean = false, + val dates: List = emptyList(), + val selectedDate: LocalDate? = null, + val selectedPlaceId: String? = null, + val bookingSuccess: Boolean = false +) { + val availablePlaces: List + get() = dates.firstOrNull { it.date == selectedDate }?.places ?: emptyList() + + val isEmpty: Boolean + get() = dates.isEmpty() } \ 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 index 1456461..2128950 100644 --- 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 @@ -1,39 +1,141 @@ package ru.myitschool.work.ui.screen.book import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope 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.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase +import java.time.LocalDate +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class BookingDate( + val date: LocalDate, + val places: List +) + +data class BookingPlace( + val id: String, + val title: String +) + class BookViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(BookState.Data) - val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _state = MutableStateFlow(BookState()) + val state = _state.asStateFlow() - fun onIntent(intent: BookIntent) { -// when (intent) { -// is MainIntent.Send -> { -// viewModelScope.launch(Dispatchers.Default) { -// _uiState.update { MainState.Loading } -// checkAndSaveAuthCodeUseCase.invoke("9999").fold( -// onSuccess = { -// _actionFlow.emit(Unit) -// }, -// onFailure = { error -> -// error.printStackTrace() -// _actionFlow.emit(Unit) -// } -// ) -// } -// } -// is MainIntent.TextInput -> Unit -// } + fun accept(intent: BookIntent) { + when (intent) { + is BookIntent.Load -> load() + is BookIntent.SelectDate -> selectDate(intent.date) + is BookIntent.SelectPlace -> selectPlace(intent.placeId) + is BookIntent.ConfirmBooking -> book() + is BookIntent.Refresh -> load() + is BookIntent.Back -> {} // обработка навигации снаружи + } + } + + private fun load() { + viewModelScope.launch { + _state.value = BookState(loading = true) + + delay(400) // имитация ожидания API + + val result = fakeApi() + + if (result.isEmpty()) { + _state.value = BookState( + dates = emptyList(), + loading = false + ) + return@launch + } + + val earliest = result.minBy { it.date } + + _state.value = BookState( + dates = result, + selectedDate = earliest.date, + loading = false + ) + } + } + + private fun selectDate(date: LocalDate) { + _state.value = _state.value.copy( + selectedDate = date, + selectedPlaceId = null + ) + } + + private fun selectPlace(placeId: String) { + _state.value = _state.value.copy(selectedPlaceId = placeId) + } + + private fun book() { + viewModelScope.launch { + _state.value = _state.value.copy(loading = true) + + delay(300) // фейковое бронирование + + _state.value = _state.value.copy( + bookingSuccess = true, + loading = false + ) + } + } + + private fun fakeApi(): List { + return listOf( + BookingDate( + LocalDate.of(2025, 1, 5), + listOf( + BookingPlace("1", "Окно №1"), + BookingPlace("2", "Окно №3") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 6), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 9), + emptyList() // будет скрыто + ), + BookingDate( + LocalDate.of(2025, 1, 10), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 11), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 12), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 13), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + ).filter { it.places.isNotEmpty() } } } \ 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 0e9895d..998160c 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 @@ -16,36 +16,40 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R import ru.myitschool.work.core.TestIds -import ru.myitschool.work.ui.nav.MainScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination + +data class Booking( + val date: String, + val place: String +) @Composable -fun MainScreen( - viewModel: MainViewModel = viewModel(), - navController: NavController +fun BookCard( + date: String, + place: String, + modifier: Modifier = Modifier ) { - val state by viewModel.uiState.collectAsState() - /* По умолчанию скрытое текстовое поле с ошибкой (main_error). @@ -59,143 +63,6 @@ fun MainScreen( Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). */ - val bookings = listOf( - Booking(date = "2025-12-01", place = "Аудитория 1"), - Booking(date = "2025-12-01", place = "Аудитория 2"), - Booking(date = "2025-12-02", place = "Аудитория 3"), - Booking(date = "2025-12-02", place = "Конференц-зал"), - Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), - Booking(date = "2025-12-03", place = "Лаборатория №101"), - Booking(date = "2025-12-04", place = "Переговорная комната"), - Booking(date = "2025-12-04", place = "Спортивный зал"), - ) - - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding( - start = 20.dp, - top = 20.dp, - end = 20.dp, - bottom = 0.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(60.dp) - .clip(CircleShape) - .testTag(TestIds.Main.PROFILE_IMAGE) - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = "Иванов Иван Иванович", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .testTag(TestIds.Main.PROFILE_NAME) - ) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.LOGOUT_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError - ), - onClick = { - }, - ) { - Text(stringResource(R.string.logout)) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ), - onClick = { - }, - ) { - Text(stringResource(R.string.refresh)) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.ADD_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFF2E7D32), - contentColor = Color.White - ), - onClick = { - }, - ) { - Text(stringResource(R.string.book_new)) - } - - Scaffold( - modifier = Modifier.fillMaxSize() - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - itemsIndexed( - items = bookings, - key = { index, item -> "main_book_pos_$index" } - ) { index, booking -> - BookCard( - date = booking.date, - place = booking.place, - modifier = Modifier.padding( - top = if (index == 0) 0.dp else 8.dp, - bottom = if (index == bookings.lastIndex) 0.dp else 8.dp - ) - ) - } - } - } - } -} - -data class Booking( - val date: String, - val place: String -) - -@Composable -fun BookCard( - date: String, - place: String, - modifier: Modifier = Modifier -) { val formattedDate = remember(date) { try { val parts = date.split("-") @@ -239,4 +106,134 @@ fun BookCard( ) } } +} + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = { /* обновить данные */ } + ) { + Text(stringResource(R.string.refresh)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + when (state) { + is MainState.Error -> { + Text( + text = (state as MainState.Error).message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + is MainState.Loading -> { + CircularProgressIndicator() + } + is MainState.Data -> { + MainContent(navController = navController) + } + } + } +} + +@Composable +fun MainContent(navController: NavController) { + val bookings = listOf( + Booking(date = "2025-12-01", place = "Аудитория 1"), + Booking(date = "2025-12-01", place = "Аудитория 2"), + Booking(date = "2025-12-02", place = "Аудитория 3"), + Booking(date = "2025-12-02", place = "Конференц-зал"), + Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), + Booking(date = "2025-12-03", place = "Лаборатория №101"), + Booking(date = "2025-12-04", place = "Переговорная комната"), + Booking(date = "2025-12-04", place = "Спортивный зал"), + ) + + Column( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(60.dp) + .clip(CircleShape) + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = "Иванов Иван Иванович", + style = MaterialTheme.typography.titleLarge + ) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ), + onClick = { /* выход */ } + ) { + Text(stringResource(R.string.logout)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { navController.navigate(BookScreenDestination) } + ) { + Text(stringResource(R.string.book_new)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Scaffold( + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + itemsIndexed( + items = bookings, + key = { index, item -> "main_book_pos_$index" } + ) { index, booking -> + BookCard( + date = booking.date, + place = booking.place, + modifier = Modifier.padding( + top = if (index == 0) 0.dp else 4.dp, + bottom = if (index == bookings.lastIndex) 0.dp else 4.dp + ) + ) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt index 67ec049..680d44a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -1,5 +1,7 @@ package ru.myitschool.work.ui.screen.main sealed interface MainState { - object Data: MainState + data object Data : MainState + data object Loading : MainState + data class Error(val message: String) : MainState } \ No newline at end of file