From 053a916b55eb7e45004cb6eb7d316e3dcb3389d7 Mon Sep 17 00:00:00 2001 From: nicktun Date: Mon, 1 Dec 2025 17:59:10 +0300 Subject: [PATCH] Book screen frontend partially implemented --- app/build.gradle.kts | 1 + .../work/ui/screen/NavigationGraph.kt | 11 +- .../work/ui/screen/auth/AuthState.kt | 1 - .../work/ui/screen/book/BookIntent.kt | 7 + .../work/ui/screen/book/BookScreen.kt | 221 ++++++++++++++++++ .../work/ui/screen/book/BookState.kt | 8 + .../work/ui/screen/book/BookViewModel.kt | 27 +++ .../work/ui/screen/main/MainIntent.kt | 1 + .../work/ui/screen/main/MainScreen.kt | 31 ++- .../work/ui/screen/main/MainViewModel.kt | 9 + 10 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f82afb..53550bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,7 @@ android { } dependencies { + implementation("androidx.compose.material3:material3:1.4.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/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 0df5e00..a87d511 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 @@ -23,6 +23,7 @@ import ru.myitschool.work.ui.nav.SplashScreenDestination import ru.myitschool.work.ui.root.RootState import ru.myitschool.work.ui.screen.auth.AuthIntent import ru.myitschool.work.ui.screen.auth.AuthScreen +import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.main.MainScreen import ru.myitschool.work.ui.screen.splash.SplashScreen @@ -38,8 +39,8 @@ fun AppNavHost( NavHost( modifier = modifier, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, +// enterTransition = { EnterTransition.None }, +// exitTransition = { ExitTransition.None }, navController = navController, startDestination = startDestination, ) { @@ -53,11 +54,7 @@ fun AppNavHost( MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "BOOK") - } + BookScreen(navController = navController) } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt index 6770dc1..84aa7e3 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt @@ -3,6 +3,5 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthState { object Loading: AuthState object Data: AuthState - object Error: AuthState } \ 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..f947f3e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookIntent { + data object Fetch: BookIntent + data object Book: BookIntent + data object GoBack: 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 new file mode 100644 index 0000000..f306382 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,221 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.BookmarkAdd +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Tab +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.mutableStateOf +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.nav.MainScreenDestination + +@Composable +fun BookScreen( + navController: NavController, + viewModel: BookViewModel = viewModel() +) { + val state by viewModel.uiState.collectAsState() + val dates = remember { bookingsByDate.keys.sorted() } + var selectedTabIndex by remember { mutableStateOf(0) } + + Box( + modifier = Modifier + .fillMaxSize() + ){ + when(val currentState = state) { + is BookState.Loading -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularProgressIndicator() + } + } + + is BookState.Error -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = "TEST_ERROR", + modifier = Modifier.testTag(TestIds.Book.ERROR), + color = MaterialTheme.colorScheme.error + ) + } + + FloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.Fetch) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = -16.dp, y = -16.dp) + .testTag(TestIds.Book.REFRESH_BUTTON) + ) { + Icon(Icons.Default.Refresh, contentDescription = "Обновить") + } + } + + is BookState.DataPresent -> { + Column(modifier = Modifier.fillMaxSize()) { + PrimaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 16.dp, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + ) { + dates.forEachIndexed { index, date -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { + Text( + text = date, + modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) + ) + }, + modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + } + } + + val selectedDate = dates[selectedTabIndex] + val bookings = bookingsByDate[selectedDate] ?: emptyList() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed(bookings) { index, booking -> + Booking(booking, index) + } + } + } + + ExtendedFloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.Book) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + text = { + Text("Бронировать") + }, + icon = { + Icon(Icons.Default.BookmarkAdd, contentDescription = "Бронировать") + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = -16.dp, y = -16.dp) + .testTag(TestIds.Book.BOOK_BUTTON) + ) + } + + is BookState.DataAbsent -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text(text = "Всё забронировано", modifier = Modifier.testTag(TestIds.Book.EMPTY)) + } + } + } + + + FloatingActionButton( + onClick = { viewModel.onIntent(BookIntent.GoBack) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .align(Alignment.BottomStart) + .offset(x = 16.dp, y = -16.dp) + .testTag(TestIds.Book.BACK_BUTTON) + ) { + Icon(Icons.Default.ArrowBackIosNew, contentDescription = "Назад") + } + } + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } +} + +@Composable +private fun Booking(booking: Booking, index: Int){ + Row( + modifier = Modifier + .fillMaxWidth() +// .clickable { } + .padding(horizontal = 16.dp, vertical = 8.dp) + .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = false, + onClick = {}, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) +// .selectable( +// selected = false, +// onClick = {} +// ) + ) + Text( + text = booking.place, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + } + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 1.dp, + modifier = Modifier.padding(start = 16.dp) + ) +} + +data class Booking(val id: Int, val place: String) +typealias BookingsByDate = Map> + +val bookingsByDate: BookingsByDate = mapOf( + "2025-01-05" to listOf(Booking(1, "102"), Booking(2, "209.13")), + "2025-01-06" to listOf(Booking(3, "Зона 51. 50")), + "2025-01-07" to listOf(Booking(1, "102"), Booking(2, "209.13")), + "2025-01-08" to listOf(Booking(2, "209.13")) +) \ 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..8edecf7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookState { + data object Loading: BookState + data object DataPresent: BookState + data object DataAbsent: BookState + data object Error: 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..f1e46f6 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,27 @@ +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 + +class BookViewModel(): ViewModel() { + private val _uiState = MutableStateFlow(BookState.DataPresent) + val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + fun onIntent(intent: BookIntent) { + when(intent) { + is BookIntent.Fetch -> Unit + is BookIntent.Book -> Unit + is BookIntent.GoBack -> viewModelScope.launch { + _actionFlow.emit(Unit) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt index 32a5f41..e892f17 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.main sealed interface MainIntent { data object Fetch: MainIntent data object Logout: MainIntent + data object NewBooking: MainIntent } \ 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 e02e484..5aa3d78 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 @@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Logout import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.BookmarkBorder +import androidx.compose.material.icons.filled.Bookmarks import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator @@ -49,6 +50,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.rememberAsyncImagePainter import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.nav.MainScreenDestination @Composable fun MainScreen( @@ -82,7 +85,26 @@ fun MainScreen( when (val currentState = state) { is MainState.Error -> { - Text("TEST_ERROR", modifier = Modifier.testTag(TestIds.Main.ERROR)) + Text( + text = "TEST_ERROR", + modifier = Modifier.testTag(TestIds.Main.ERROR), + color = MaterialTheme.colorScheme.error + ) + IconButton( + onClick = { viewModel.onIntent(MainIntent.Fetch) }, + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .testTag(TestIds.Main.REFRESH_BUTTON), + enabled = true, + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) + } } is MainState.Loading -> { @@ -176,7 +198,6 @@ fun MainScreen( Icon( imageVector = Icons.Default.BookmarkBorder, contentDescription = null, - tint = MaterialTheme.colorScheme.secondaryFixed ) Text( text = "Бронирования", @@ -213,7 +234,7 @@ fun MainScreen( } FloatingActionButton( - onClick = { }, + onClick = { viewModel.onIntent(MainIntent.NewBooking) }, containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier @@ -223,7 +244,6 @@ fun MainScreen( ) { Icon(Icons.Default.Add, contentDescription = "Добавить") } - } } } @@ -231,6 +251,9 @@ fun MainScreen( LaunchedEffect(Unit) { viewModel.onIntent(MainIntent.Fetch) + viewModel.actionFlow.collect { + navController.navigate(BookScreenDestination) + } } } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt index dfa3af0..41c0e3f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -2,7 +2,9 @@ package ru.myitschool.work.ui.screen.main 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 @@ -13,6 +15,8 @@ class MainViewModel(): ViewModel() { private val logout by lazy { Logout(AuthRepository) } private val _uiState = MutableStateFlow(MainState.Data) val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: MainIntent) { when (intent) { @@ -22,6 +26,11 @@ class MainViewModel(): ViewModel() { logout.invoke() } } + is MainIntent.NewBooking -> { + viewModelScope.launch { + _actionFlow.emit(Unit) + } + } } } } \ No newline at end of file