From 2807368adba470136d627a38a162fd7264646b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Sun, 30 Nov 2025 17:52:39 +0300 Subject: [PATCH 1/6] Added appearance and basic booking functionality --- .../work/domain/entities/BookingEntities.kt | 6 + .../work/domain/entities/UserEntities.kt | 7 + .../work/ui/screen/NavigationGraph.kt | 7 +- .../work/ui/screen/book/BookIntent.kt | 6 + .../work/ui/screen/book/BookScreen.kt | 215 ++++++++++++++++++ .../work/ui/screen/book/BookState.kt | 8 + .../work/ui/screen/book/BookViewModel.kt | 31 +++ app/src/main/res/values/strings.xml | 2 + 8 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt 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/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt new file mode 100644 index 0000000..9cc4cad --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.domain.entities + +data class BookingEntities ( + var roomName : String, + var time: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt new file mode 100644 index 0000000..23d2aba --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.domain.entities + +data class UserEntities( + val name : String, + var image : Int, + var booking : ArrayList +) 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 01b0f32..9dad87b 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 @@ -15,6 +15,7 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination 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 @Composable fun AppNavHost( @@ -39,11 +40,7 @@ fun AppNavHost( } } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + BookScreen(navController = navController) } } } \ 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..506f720 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookIntent { + data class Send(val text: String): BookIntent + data class BookingSelect(val text: 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 new file mode 100644 index 0000000..7dc159f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,215 @@ +package ru.myitschool.work.ui.screen.book; + +import androidx.compose.foundation.layout.Arrangement +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.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.RadioButton +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController; +import ru.myitschool.work.R +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.domain.entities.BookingEntities +import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.nav.MainScreenDestination +import ru.myitschool.work.ui.screen.auth.AuthViewModel + +var selectedTime = mutableStateOf(0) +var selectedBooking = mutableStateOf(0) +var currentTime = mutableStateOf(0) +@Composable +fun BookScreen( + viewModel: BookViewModel = viewModel(), + navController: NavController, + modifier: Modifier = Modifier +) { + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } + + // TODO брать данные с сервера + // Иммитация того, что мы взяли данные с сервера + val bookings = arrayListOf( + BookingEntities( + "Рабочее место у окна", + "19.04" + ), + BookingEntities( + "Переговорная комната № 1", + "19.04" + ), + BookingEntities( + "Коворкинг А", + "19.04" + ), + BookingEntities( + "Кабинет № 33", + "20.04" + ), + ) + val options = toMap(bookings) + + Column( + modifier = modifier.fillMaxSize() + ) { +// Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) + when (val currentState = state) { + BookState.Data -> { + TabGroup(options.keys) + + var i = 0 + options.keys.forEach { + if (i == currentTime.value) + SelectBooking(options[it]!!) + i ++; + } + + Button( + onClick = { + // TODO Добавить бронирование + viewModel.onIntent(BookIntent.Send("Данные" )) + }, + modifier = Modifier + .testTag(TestIds.Book.BOOK_BUTTON) + ) { + Text(stringResource(R.string.to_book)) + } + } + + BookState.Error -> { + Text( + text = "", + modifier = Modifier + .testTag(TestIds.Book.ERROR) + ) + + Button( + onClick = { }, + modifier = Modifier + .testTag(TestIds.Book.REFRESH_BUTTON) + ) { + Text("") + } + } + + BookState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(64.dp) + ) + } + + BookState.NotData -> { + Text( + text = "", + modifier = Modifier + .testTag(TestIds.Book.EMPTY) + ) + } + } + Button( + onClick = { + navController.navigate(MainScreenDestination) + }, + modifier = Modifier + .testTag(TestIds.Book.BACK_BUTTON) + ) { + Text(stringResource(R.string.back)) + } + } +} + + +@Composable +fun TabGroup(options: Set) { + NavigationBar( + Modifier.fillMaxWidth() + ) { + options.forEachIndexed { index, label -> + NavigationBarItem( + selected = currentTime.value == index, + onClick = { + currentTime.value = index + }, + icon = { + Text( + text = label, + modifier = Modifier + .testTag(TestIds.Book.ITEM_DATE) + ) + }, + modifier = Modifier + .testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + } + } +} + +@Composable +fun SelectBooking(options: List) { + LazyColumn( + Modifier + .fillMaxWidth() + ) { + options.forEachIndexed { index, label -> + item { + Row( + Modifier + .fillMaxWidth() + .selectableGroup() + .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + RadioButton( + selected = index == selectedBooking.value && currentTime.value == selectedTime.value, + onClick = { + selectedBooking.value = index + selectedTime.value = currentTime.value + }, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) + ) + } + } + } + } +} + +fun toMap(options: List) : Map> { + val map : MutableMap> = mutableMapOf() + options.forEach { + if (map[it.time] == null) map[it.time] = mutableListOf(it.roomName) + else map[it.time]?.add(it.roomName) + } + return map +} \ 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..8b87d87 --- /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 { + object Loading: BookState + object Data: BookState + object Error: BookState + object NotData : 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..c713202 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,31 @@ +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 + +class BookViewModel : ViewModel() { + private val _uiState = MutableStateFlow(BookState.Data) + val uiState: StateFlow = _uiState.asStateFlow(); + + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + fun onIntent(intent: BookIntent) { + when (intent) { + is BookIntent.Send -> { + viewModelScope.launch(Dispatchers.Default) { + _uiState.update { BookState.Loading } + } + } + is BookIntent.BookingSelect -> Unit + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa8bda6..5979b52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,6 @@ Привет! Введи код для авторизации Код Войти + забронировать + Назад \ No newline at end of file -- 2.34.1 From 47e9018b672ea532333742101272df039a0f524a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2?= Date: Sun, 30 Nov 2025 16:05:59 +0000 Subject: [PATCH 2/6] revert abc0f81356d0f4024ba343c91756f236c96876f2 revert Merge pull request 'Added appearance and basic booking functionality' (#1) from student-15047/NTO-2025-Android-TeamTask:to-book into main Reviewed-on: https://git.sicampus.ru/student-18211/NTO-2025-Android-TeamTask/pulls/1 --- .../work/domain/entities/BookingEntities.kt | 6 - .../work/domain/entities/UserEntities.kt | 7 - .../work/ui/screen/NavigationGraph.kt | 7 +- .../work/ui/screen/book/BookIntent.kt | 6 - .../work/ui/screen/book/BookScreen.kt | 215 ------------------ .../work/ui/screen/book/BookState.kt | 8 - .../work/ui/screen/book/BookViewModel.kt | 31 --- app/src/main/res/values/strings.xml | 2 - 8 files changed, 5 insertions(+), 277 deletions(-) delete mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt delete mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt deleted file mode 100644 index 9cc4cad..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class BookingEntities ( - var roomName : String, - var time: String -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt deleted file mode 100644 index 23d2aba..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class UserEntities( - val name : String, - var image : Int, - var booking : ArrayList -) 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 9dad87b..01b0f32 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 @@ -15,7 +15,6 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination 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 @Composable fun AppNavHost( @@ -40,7 +39,11 @@ fun AppNavHost( } } composable { - BookScreen(navController = navController) + Box( + contentAlignment = Alignment.Center + ) { + Text(text = "Hello") + } } } } \ 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 deleted file mode 100644 index 506f720..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -sealed interface BookIntent { - data class Send(val text: String): BookIntent - data class BookingSelect(val text: 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 deleted file mode 100644 index 7dc159f..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ /dev/null @@ -1,215 +0,0 @@ -package ru.myitschool.work.ui.screen.book; - -import androidx.compose.foundation.layout.Arrangement -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.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.RadioButton -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.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController; -import ru.myitschool.work.R -import ru.myitschool.work.core.TestIds -import ru.myitschool.work.domain.entities.BookingEntities -import ru.myitschool.work.ui.nav.BookScreenDestination -import ru.myitschool.work.ui.nav.MainScreenDestination -import ru.myitschool.work.ui.screen.auth.AuthViewModel - -var selectedTime = mutableStateOf(0) -var selectedBooking = mutableStateOf(0) -var currentTime = mutableStateOf(0) -@Composable -fun BookScreen( - viewModel: BookViewModel = viewModel(), - navController: NavController, - modifier: Modifier = Modifier -) { - val state by viewModel.uiState.collectAsState() - - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - - // TODO брать данные с сервера - // Иммитация того, что мы взяли данные с сервера - val bookings = arrayListOf( - BookingEntities( - "Рабочее место у окна", - "19.04" - ), - BookingEntities( - "Переговорная комната № 1", - "19.04" - ), - BookingEntities( - "Коворкинг А", - "19.04" - ), - BookingEntities( - "Кабинет № 33", - "20.04" - ), - ) - val options = toMap(bookings) - - Column( - modifier = modifier.fillMaxSize() - ) { -// Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) - when (val currentState = state) { - BookState.Data -> { - TabGroup(options.keys) - - var i = 0 - options.keys.forEach { - if (i == currentTime.value) - SelectBooking(options[it]!!) - i ++; - } - - Button( - onClick = { - // TODO Добавить бронирование - viewModel.onIntent(BookIntent.Send("Данные" )) - }, - modifier = Modifier - .testTag(TestIds.Book.BOOK_BUTTON) - ) { - Text(stringResource(R.string.to_book)) - } - } - - BookState.Error -> { - Text( - text = "", - modifier = Modifier - .testTag(TestIds.Book.ERROR) - ) - - Button( - onClick = { }, - modifier = Modifier - .testTag(TestIds.Book.REFRESH_BUTTON) - ) { - Text("") - } - } - - BookState.Loading -> { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(64.dp) - ) - } - - BookState.NotData -> { - Text( - text = "", - modifier = Modifier - .testTag(TestIds.Book.EMPTY) - ) - } - } - Button( - onClick = { - navController.navigate(MainScreenDestination) - }, - modifier = Modifier - .testTag(TestIds.Book.BACK_BUTTON) - ) { - Text(stringResource(R.string.back)) - } - } -} - - -@Composable -fun TabGroup(options: Set) { - NavigationBar( - Modifier.fillMaxWidth() - ) { - options.forEachIndexed { index, label -> - NavigationBarItem( - selected = currentTime.value == index, - onClick = { - currentTime.value = index - }, - icon = { - Text( - text = label, - modifier = Modifier - .testTag(TestIds.Book.ITEM_DATE) - ) - }, - modifier = Modifier - .testTag(TestIds.Book.getIdDateItemByPosition(index)) - ) - } - } -} - -@Composable -fun SelectBooking(options: List) { - LazyColumn( - Modifier - .fillMaxWidth() - ) { - options.forEachIndexed { index, label -> - item { - Row( - Modifier - .fillMaxWidth() - .selectableGroup() - .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_TEXT) - ) - RadioButton( - selected = index == selectedBooking.value && currentTime.value == selectedTime.value, - onClick = { - selectedBooking.value = index - selectedTime.value = currentTime.value - }, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) - ) - } - } - } - } -} - -fun toMap(options: List) : Map> { - val map : MutableMap> = mutableMapOf() - options.forEach { - if (map[it.time] == null) map[it.time] = mutableListOf(it.roomName) - else map[it.time]?.add(it.roomName) - } - return map -} \ 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 deleted file mode 100644 index 8b87d87..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -sealed interface BookState { - object Loading: BookState - object Data: BookState - object Error: BookState - object NotData : 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 deleted file mode 100644 index c713202..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -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 - -class BookViewModel : ViewModel() { - private val _uiState = MutableStateFlow(BookState.Data) - val uiState: StateFlow = _uiState.asStateFlow(); - - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow - - fun onIntent(intent: BookIntent) { - when (intent) { - is BookIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { - _uiState.update { BookState.Loading } - } - } - is BookIntent.BookingSelect -> Unit - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5979b52..fa8bda6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,4 @@ Привет! Введи код для авторизации Код Войти - забронировать - Назад \ No newline at end of file -- 2.34.1 From 09e97f9d67d932249d63cf01a899de3e36d0a6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Thu, 4 Dec 2025 23:19:42 +0300 Subject: [PATCH 3/6] I configured the server connection and improved the logic --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/repo/BookRepository.kt | 15 +++ .../work/data/source/NetworkDataSource.kt | 41 ++++++ .../work/domain/entities/BookingEntities.kt | 6 - .../work/domain/entities/UserEntities.kt | 7 - .../work/ui/screen/BookingUserInfo.kt | 16 +++ .../work/ui/screen/NavigationGraph.kt | 7 +- .../work/ui/screen/book/BookIntent.kt | 8 +- .../work/ui/screen/book/BookScreen.kt | 124 +++++++++--------- .../work/ui/screen/book/BookState.kt | 10 +- .../work/ui/screen/book/BookViewModel.kt | 98 +++++++++++++- app/src/main/res/values/strings.xml | 2 + 12 files changed, 250 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt delete mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt delete mode 100644 app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index a8b7cc5..1aae4fd 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://10.0.2.2:8080" + const val HOST = "http://127.0.0.1:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt new file mode 100644 index 0000000..2887f8e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.ui.screen.UserInfo + +object BookRepository { + suspend fun loadBooks(code: String): Result> { + return NetworkDataSource.getFreeBooking(code) + } + + suspend fun sendData(code: String, booking: Booking) : Result { + return NetworkDataSource.createNewBooking(code, booking) + } +} \ No newline at end of file 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 fbdfef5..62c7504 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 @@ -1,16 +1,23 @@ package ru.myitschool.work.data.source import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get +import io.ktor.client.request.post import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.ui.screen.UserInfo object NetworkDataSource { private val client by lazy { @@ -31,6 +38,40 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) + when (response.status) { + HttpStatusCode.OK -> true + else -> error("Неверный код для авторизации") + } + } + } + + suspend fun getInfo(code: String): Result = withContext(Dispatchers.IO){ + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when(response.status){ + HttpStatusCode.OK -> response.body() + else -> error("Ошибка получения данных") + } + } + } + + suspend fun getFreeBooking(code: String) : Result> = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> response.body() + else -> error(response.bodyAsText()) + } + } + } + + @OptIn(InternalAPI::class) + suspend fun createNewBooking(code: String, booking: Booking) : Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.post(getUrl(code, Constants.BOOK_URL)) { + contentType(ContentType.Application.Json) + body = booking + } when (response.status) { HttpStatusCode.OK -> true else -> error(response.bodyAsText()) diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt deleted file mode 100644 index 9cc4cad..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntities.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class BookingEntities ( - var roomName : String, - var time: String -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt deleted file mode 100644 index 23d2aba..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntities.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.myitschool.work.domain.entities - -data class UserEntities( - val name : String, - var image : Int, - var booking : ArrayList -) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt new file mode 100644 index 0000000..39c3e4b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.ui.screen + +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val name: String, + val photo: String? = null, + val bookings: List +) + +@Serializable +data class Booking( + val date: String, + val place: String +) \ No newline at end of file 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 9dad87b..b70bbc0 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 @Composable fun AppNavHost( @@ -40,7 +42,10 @@ fun AppNavHost( } } composable { - BookScreen(navController = navController) + BookScreen( + viewModel = BookViewModel(), + navController = navController + ) } } } \ 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 index 506f720..8d77d08 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,10 @@ package ru.myitschool.work.ui.screen.book +import ru.myitschool.work.ui.screen.Booking + sealed interface BookIntent { - data class Send(val text: String): BookIntent - data class BookingSelect(val text: String): BookIntent + data class Send(val code : String, val booking: Booking) : BookIntent + data object BackToMainScreen : BookIntent + data object ToAuthScreen : BookIntent + data object LoadData : 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 7dc159f..47dd7ad 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,5 +1,6 @@ package ru.myitschool.work.ui.screen.book; +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,12 +11,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.RadioButton import androidx.compose.material3.Text -import androidx.compose.runtime.Composable; +import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,17 +28,17 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController; +import androidx.navigation.NavController import ru.myitschool.work.R import ru.myitschool.work.core.TestIds -import ru.myitschool.work.domain.entities.BookingEntities -import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination -import ru.myitschool.work.ui.screen.auth.AuthViewModel +import ru.myitschool.work.ui.screen.Booking -var selectedTime = mutableStateOf(0) -var selectedBooking = mutableStateOf(0) -var currentTime = mutableStateOf(0) +var selectedTime: MutableState = mutableStateOf(null) +var selectedBooking: MutableState = mutableStateOf(null) +var currentTime: MutableState = mutableStateOf(null) @Composable fun BookScreen( viewModel: BookViewModel = viewModel(), @@ -45,73 +48,72 @@ fun BookScreen( val state by viewModel.uiState.collectAsState() LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + Log.d("BookScreen", "1") + viewModel.navigationFlow.collect { + // TODO настроить наконец это переход между экранами +// Log.d("BookScreen", "2") + when (it) { + BookNavigationEvent.NavigateToMain -> { + Log.d("BookScreen", "3") + navController.navigate(MainScreenDestination) + } + BookNavigationEvent.NavigateToAuth -> { + Log.d("BookScreen", "4") + navController.navigate(AuthScreenDestination) { + Log.d("BookScreen", "5") + popUpTo(navController.graph.startDestinationId) { + Log.d("BookScreen", "6") + inclusive = true + } + } + } + } } } - // TODO брать данные с сервера - // Иммитация того, что мы взяли данные с сервера - val bookings = arrayListOf( - BookingEntities( - "Рабочее место у окна", - "19.04" - ), - BookingEntities( - "Переговорная комната № 1", - "19.04" - ), - BookingEntities( - "Коворкинг А", - "19.04" - ), - BookingEntities( - "Кабинет № 33", - "20.04" - ), - ) - val options = toMap(bookings) - Column( modifier = modifier.fillMaxSize() ) { // Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) - when (val currentState = state) { - BookState.Data -> { + when (state) { + is BookState.Data -> { + val options = (state as BookState.Data).booking + if (currentTime.value == null) + for (el in options) { + currentTime.value = el.key + break + } TabGroup(options.keys) - - var i = 0 - options.keys.forEach { - if (i == currentTime.value) - SelectBooking(options[it]!!) - i ++; - } + options[currentTime.value]?.let { SelectBooking(it) } Button( onClick = { - // TODO Добавить бронирование - viewModel.onIntent(BookIntent.Send("Данные" )) + viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedBooking.value!!)) }, modifier = Modifier - .testTag(TestIds.Book.BOOK_BUTTON) + .testTag(TestIds.Book.BOOK_BUTTON), + enabled = selectedBooking.value != null ) { Text(stringResource(R.string.to_book)) } } - BookState.Error -> { + is BookState.Error -> { Text( - text = "", + text = (state as BookState.Error).error, + color = MaterialTheme.colorScheme.error, modifier = Modifier .testTag(TestIds.Book.ERROR) ) Button( - onClick = { }, + onClick = { + viewModel.onIntent(BookIntent.LoadData) + }, modifier = Modifier .testTag(TestIds.Book.REFRESH_BUTTON) ) { - Text("") + Text(stringResource(R.string.upadate)) // А что сюда писать? } } @@ -125,7 +127,7 @@ fun BookScreen( BookState.NotData -> { Text( - text = "", + text = stringResource(R.string.not_book), modifier = Modifier .testTag(TestIds.Book.EMPTY) ) @@ -133,7 +135,7 @@ fun BookScreen( } Button( onClick = { - navController.navigate(MainScreenDestination) + viewModel.onIntent(BookIntent.BackToMainScreen) }, modifier = Modifier .testTag(TestIds.Book.BACK_BUTTON) @@ -151,9 +153,9 @@ fun TabGroup(options: Set) { ) { options.forEachIndexed { index, label -> NavigationBarItem( - selected = currentTime.value == index, + selected = currentTime.value == label, onClick = { - currentTime.value = index + currentTime.value = label }, icon = { Text( @@ -170,12 +172,12 @@ fun TabGroup(options: Set) { } @Composable -fun SelectBooking(options: List) { +fun SelectBooking(options: List) { LazyColumn( Modifier .fillMaxWidth() ) { - options.forEachIndexed { index, label -> + options.forEachIndexed { index, book -> item { Row( Modifier @@ -186,14 +188,14 @@ fun SelectBooking(options: List) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = label, + text = book.place, modifier = Modifier .testTag(TestIds.Book.ITEM_PLACE_TEXT) ) RadioButton( - selected = index == selectedBooking.value && currentTime.value == selectedTime.value, + selected = book == selectedBooking.value && currentTime.value == selectedTime.value, onClick = { - selectedBooking.value = index + selectedBooking.value = book selectedTime.value = currentTime.value }, modifier = Modifier @@ -205,11 +207,3 @@ fun SelectBooking(options: List) { } } -fun toMap(options: List) : Map> { - val map : MutableMap> = mutableMapOf() - options.forEach { - if (map[it.time] == null) map[it.time] = mutableListOf(it.roomName) - else map[it.time]?.add(it.roomName) - } - return map -} \ 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 8b87d87..1b32a2f 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,8 +1,14 @@ package ru.myitschool.work.ui.screen.book +import ru.myitschool.work.ui.screen.Booking + sealed interface BookState { object Loading: BookState - object Data: BookState - object Error: BookState + data class Data( + val booking : Map> = mapOf() + ): BookState + data class Error( + val error : String + ): BookState object NotData : 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 index c713202..ce5a423 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,5 +1,6 @@ package ru.myitschool.work.ui.screen.book +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -10,22 +11,115 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.repo.BookRepository +import ru.myitschool.work.ui.screen.Booking class BookViewModel : ViewModel() { - private val _uiState = MutableStateFlow(BookState.Data) + private val _uiState = MutableStateFlow(BookState.Loading) val uiState: StateFlow = _uiState.asStateFlow(); private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + private val _navigationFlow = MutableSharedFlow() + val navigationFlow: SharedFlow = _navigationFlow + + init { + loadData() + } + + private fun loadData() { + _uiState.update { BookState.Loading } + viewModelScope.launch(Dispatchers.IO) { + val code = AuthRepository.getSavedCode() ?: run { + onIntent(BookIntent.ToAuthScreen) + Log.d("", "Go to AuthScreen") + return@launch + } + Log.d("", "Проверка") +// _uiState.update { +// BookState.Data( +// listOf( +// Booking( +// "19.04", +// "Рабочее место у окна" +// ), +// Booking( +// "19.04", +// "Переговорная комната № 1" +// ), +// Booking( +// "19.04", +// "Коворкинг А" +// ), +// Booking( +// "20.04", +// "Кабинет № 33" +// ), +// ).toMap() +// ) +// } + BookRepository.loadBooks(code).fold( + onSuccess = { + it.let { bookings -> + _uiState.update { + when (bookings.isEmpty()) { + true -> BookState.Data( + booking = bookings.toMap() + ) + false -> BookState.NotData + } + } + } + }, + onFailure = { error -> + _uiState.update { + BookState.Error( + error = error.message ?: "Не удалось загрузить данные" + ) + } + } + ) + } + } fun onIntent(intent: BookIntent) { when (intent) { is BookIntent.Send -> { viewModelScope.launch(Dispatchers.Default) { _uiState.update { BookState.Loading } + BookRepository.sendData(intent.code, intent.booking).fold( + onSuccess = { + _actionFlow.emit(Unit) + }, + onFailure = { error -> + BookState.Error(error.message ?: "Неизвестная ошибка") + } + ) } } - is BookIntent.BookingSelect -> Unit + BookIntent.BackToMainScreen -> { + _navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain) + } + BookIntent.LoadData -> loadData() + BookIntent.ToAuthScreen -> { + _navigationFlow.tryEmit(BookNavigationEvent.NavigateToAuth) + } } } +} + +fun List.toMap() : Map> { + val options = this + val map : MutableMap> = mutableMapOf() + options.forEach { + if (map[it.date] == null) map[it.date] = mutableListOf(it) + else map[it.date]?.add(it) + } + return map +} + +sealed interface BookNavigationEvent { + object NavigateToAuth : BookNavigationEvent + object NavigateToMain : BookNavigationEvent } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5979b52..fa27628 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,4 +6,6 @@ Войти забронировать Назад + Всё забранировано + Обновить \ No newline at end of file -- 2.34.1 From 655baf713ff090043abf9e2643162ac5345a2878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Fri, 5 Dec 2025 21:32:25 +0300 Subject: [PATCH 4/6] I made the logic for moving between screens and added stubs --- .../work/ui/screen/book/BookScreen.kt | 7 +++---- .../work/ui/screen/book/BookViewModel.kt | 20 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) 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 47dd7ad..5772cd6 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 @@ -49,10 +49,9 @@ fun BookScreen( LaunchedEffect(Unit) { Log.d("BookScreen", "1") - viewModel.navigationFlow.collect { - // TODO настроить наконец это переход между экранами -// Log.d("BookScreen", "2") - when (it) { + viewModel.navigationFlow.collect { event -> + Log.d("navigation", "Event received: $event") + when (event) { BookNavigationEvent.NavigateToMain -> { Log.d("BookScreen", "3") navController.navigate(MainScreenDestination) 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 ce5a423..841bb54 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 @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -18,21 +19,18 @@ import ru.myitschool.work.ui.screen.Booking class BookViewModel : ViewModel() { private val _uiState = MutableStateFlow(BookState.Loading) val uiState: StateFlow = _uiState.asStateFlow(); - - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow private val _navigationFlow = MutableSharedFlow() - val navigationFlow: SharedFlow = _navigationFlow + val navigationFlow: SharedFlow = _navigationFlow.asSharedFlow() init { loadData() } private fun loadData() { - _uiState.update { BookState.Loading } viewModelScope.launch(Dispatchers.IO) { + _uiState.update { BookState.Loading } val code = AuthRepository.getSavedCode() ?: run { - onIntent(BookIntent.ToAuthScreen) + _navigationFlow.emit(BookNavigationEvent.NavigateToAuth) Log.d("", "Go to AuthScreen") return@launch } @@ -90,7 +88,7 @@ class BookViewModel : ViewModel() { _uiState.update { BookState.Loading } BookRepository.sendData(intent.code, intent.booking).fold( onSuccess = { - _actionFlow.emit(Unit) + _navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain) }, onFailure = { error -> BookState.Error(error.message ?: "Неизвестная ошибка") @@ -99,11 +97,15 @@ class BookViewModel : ViewModel() { } } BookIntent.BackToMainScreen -> { - _navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain) + viewModelScope.launch { + _navigationFlow.emit(BookNavigationEvent.NavigateToMain) + } } BookIntent.LoadData -> loadData() BookIntent.ToAuthScreen -> { - _navigationFlow.tryEmit(BookNavigationEvent.NavigateToAuth) + viewModelScope.launch { + _navigationFlow.emit(BookNavigationEvent.NavigateToAuth) + } } } } -- 2.34.1 From 8a5fee532bd52728a1db7f159f435702c3646931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 8 Dec 2025 21:22:55 +0300 Subject: [PATCH 5/6] I completed all the tasks assigned to me and combined my part of the project with Egor's part. --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/repo/AuthRepository.kt | 32 ++- .../work/data/repo/BookRepository.kt | 9 +- .../work/data/repo/MainRepository.kt | 16 ++ .../work/data/source/NetworkDataSource.kt | 58 +++-- .../auth/CheckAndSaveAuthCodeUseCase.kt | 8 +- .../work/ui/screen/BookingUserInfo.kt | 28 ++- .../work/ui/screen/NavigationGraph.kt | 14 +- .../work/ui/screen/auth/AuthScreen.kt | 2 +- .../work/ui/screen/auth/AuthViewModel.kt | 4 +- .../work/ui/screen/book/BookIntent.kt | 4 +- .../work/ui/screen/book/BookScreen.kt | 154 ++++++------ .../work/ui/screen/book/BookState.kt | 5 +- .../work/ui/screen/book/BookViewModel.kt | 31 +-- .../work/ui/screen/main/MainIntent.kt | 7 + .../work/ui/screen/main/MainScreen.kt | 223 ++++++++++++++++++ .../work/ui/screen/main/MainState.kt | 16 ++ .../work/ui/screen/main/MainViewModel.kt | 102 ++++++++ 18 files changed, 570 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index 1aae4fd..690c2b0 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://127.0.0.1:8080" + const val HOST = "http://192.168.1.74:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index da5682d..1807f66 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.data.repo import android.content.Context +import android.util.Log import ru.myitschool.work.App import ru.myitschool.work.data.source.NetworkDataSource @@ -10,17 +11,32 @@ object AuthRepository { private const val PREF_NAME = "auth_prefs" private const val KEY_SAVED_CODE = "saved_code" private val context: Context get() = App.context + private fun loadSavedCode(): String? { -// return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) -// .getString(KEY_SAVED_CODE, null) - return "" + return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(KEY_SAVED_CODE, null) } + fun getSavedCode(): String? = loadSavedCode() - suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text + + suspend fun checkAndSave(text: String): Result { + return NetworkDataSource.checkAuth(text).fold( + onSuccess = { success -> + if (success) { + codeCache = text + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_SAVED_CODE, text) + .apply() + Result.success(Unit) + } else { + Result.failure(IllegalStateException("Неверный код для авторизации")) + } + }, + onFailure = { error -> + Log.e("AuthRepository", "Auth failed", error) + Result.failure(error) } - } + ) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt index 2887f8e..d36e059 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -1,15 +1,14 @@ package ru.myitschool.work.data.repo import ru.myitschool.work.data.source.NetworkDataSource -import ru.myitschool.work.ui.screen.Booking -import ru.myitschool.work.ui.screen.UserInfo +import ru.myitschool.work.ui.screen.BookingItem object BookRepository { - suspend fun loadBooks(code: String): Result> { + suspend fun loadBooks(code: String): Result>> { return NetworkDataSource.getFreeBooking(code) } - suspend fun sendData(code: String, booking: Booking) : Result { - return NetworkDataSource.createNewBooking(code, booking) + suspend fun sendData(code: String, data: String, placeId: Int) : Result { + return NetworkDataSource.createNewBooking(code, data, placeId) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt new file mode 100644 index 0000000..f768094 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.ui.screen.UserInfo +import androidx.core.content.edit + +object MainRepository { + suspend fun loadUserInfo(code: String): Result { + return NetworkDataSource.getInfo(code) + } + fun clearAuth() { + val prefs = ru.myitschool.work.App.context + .getSharedPreferences("auth_prefs", android.content.Context.MODE_PRIVATE) + prefs.edit { clear() } + } +} \ No newline at end of file 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 62c7504..aee6910 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 @@ -1,11 +1,13 @@ package ru.myitschool.work.data.source +import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get import io.ktor.client.request.post +import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode @@ -14,9 +16,12 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.ui.screen.BookingItem +import ru.myitschool.work.ui.screen.CreateBookingRequest import ru.myitschool.work.ui.screen.UserInfo object NetworkDataSource { @@ -35,49 +40,64 @@ object NetworkDataSource { } } + suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) + Log.d("NetworkDataSource", "Auth response: ${response.status}") when (response.status) { HttpStatusCode.OK -> true else -> error("Неверный код для авторизации") } + }.onFailure { error -> + Log.e("NetworkDataSource", "Auth request failed", error) } } - suspend fun getInfo(code: String): Result = withContext(Dispatchers.IO){ return@withContext runCatching { val response = client.get(getUrl(code, Constants.INFO_URL)) when(response.status){ - HttpStatusCode.OK -> response.body() + HttpStatusCode.OK -> { + response.body() + } else -> error("Ошибка получения данных") } } } - suspend fun getFreeBooking(code: String) : Result> = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.get(getUrl(code, Constants.BOOKING_URL)) - when (response.status) { - HttpStatusCode.OK -> response.body() - else -> error(response.bodyAsText()) + + + + suspend fun getFreeBooking(code: String): Result>> = + withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> { + // Используйте response.body с явным указанием типа + response.body>>() + } + else -> error(response.bodyAsText()) + } } } - } + @OptIn(InternalAPI::class) - suspend fun createNewBooking(code: String, booking: Booking) : Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.post(getUrl(code, Constants.BOOK_URL)) { - contentType(ContentType.Application.Json) - body = booking - } - when (response.status) { - HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + suspend fun createNewBooking(code: String, data: String, placeId: Int): Result = + withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.post(getUrl(code, Constants.BOOK_URL)) { + contentType(ContentType.Application.Json) + setBody(CreateBookingRequest(data, placeId)) // используйте setBody вместо body + } + + when (response.status) { + HttpStatusCode.OK -> true + else -> error(response.bodyAsText()) + } } } - } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 012fb6f..733d95c 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt @@ -5,11 +5,7 @@ import ru.myitschool.work.data.repo.AuthRepository class CheckAndSaveAuthCodeUseCase( private val repository: AuthRepository ) { - suspend operator fun invoke( - text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") - } + suspend operator fun invoke(text: String): Result { + return repository.checkAndSave(text) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt index 39c3e4b..0874863 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt @@ -5,12 +5,34 @@ import kotlinx.serialization.Serializable @Serializable data class UserInfo( val name: String, - val photo: String? = null, - val bookings: List -) + val photoUrl: String? = null, + val booking: Map> +) { + fun bookingToList() : List { + val bookings = mutableListOf() + booking.forEach { + it.value.forEach { value -> + bookings.add(Booking(it.key, value.place)) + } + } + return bookings + } +} @Serializable data class Booking( val date: String, val place: String +) + +@Serializable +data class BookingItem( + val id: Int, + val place: String +) + +@Serializable +data class CreateBookingRequest( + val date: String, + val placeId: Int ) \ No newline at end of file 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 b70bbc0..479c17c 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen +import android.util.Log import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Box @@ -18,6 +19,8 @@ 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 +import ru.myitschool.work.ui.screen.main.MainViewModel @Composable fun AppNavHost( @@ -32,18 +35,17 @@ fun AppNavHost( startDestination = AuthScreenDestination, ) { composable { + Log.d("compose", "Auth") AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + Log.d("compose", "Main") + MainScreen(viewModel = viewModel(), navController) } composable { + Log.d("compose", "Book") BookScreen( - viewModel = BookViewModel(), + viewModel = viewModel(), navController = navController ) } 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 ccbb6df..e22a5e4 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 @@ -75,7 +75,7 @@ private fun Content( ) { var inputText by remember { mutableStateOf("") } - val isValidCode = inputText.length >= 4 && inputText.isNotEmpty() && inputText.none { it.isWhitespace() } && inputText.all { ch -> + val isValidCode = inputText.length == 4 && inputText.isNotEmpty() && inputText.none { it.isWhitespace() } && inputText.all { ch -> ch in '0'..'9' || ch in 'A'..'Z' || ch in 'a'..'z' } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index fe7d469..e6d3779 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -24,11 +24,11 @@ class AuthViewModel : ViewModel() { fun onIntent(intent: AuthIntent) { when (intent) { is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { _uiState.update { AuthState.Loading } checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { - _actionFlow.emit(Unit)// переход на MainScreen + _actionFlow.emit(Unit) }, onFailure = { error -> _uiState.update { 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 8d77d08..d983c38 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,9 +1,7 @@ package ru.myitschool.work.ui.screen.book -import ru.myitschool.work.ui.screen.Booking - sealed interface BookIntent { - data class Send(val code : String, val booking: Booking) : BookIntent + data class Send(val code : String, val data: String, val placeId : Int) : BookIntent data object BackToMainScreen : BookIntent data object ToAuthScreen : BookIntent data object LoadData : BookIntent 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 5772cd6..b76cd92 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 @@ -18,10 +18,11 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState 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.platform.testTag @@ -34,11 +35,10 @@ import ru.myitschool.work.core.TestIds import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination -import ru.myitschool.work.ui.screen.Booking +import java.time.LocalDate +import java.time.format.DateTimeFormatter + -var selectedTime: MutableState = mutableStateOf(null) -var selectedBooking: MutableState = mutableStateOf(null) -var currentTime: MutableState = mutableStateOf(null) @Composable fun BookScreen( viewModel: BookViewModel = viewModel(), @@ -76,22 +76,93 @@ fun BookScreen( // Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) when (state) { is BookState.Data -> { + var selectedTime : String? by remember { mutableStateOf(null) } + var selectedBooking : Int? by remember { mutableStateOf(null) } + var currentTime : String? by remember { mutableStateOf(null) } + val options = (state as BookState.Data).booking - if (currentTime.value == null) + + if (currentTime == null) for (el in options) { - currentTime.value = el.key - break + if (!el.value.isEmpty()) { + currentTime = el.key + break + } } - TabGroup(options.keys) - options[currentTime.value]?.let { SelectBooking(it) } + + NavigationBar( + Modifier.fillMaxWidth() + ) { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM") + + options.keys + .map { LocalDate.parse(it, inputFormatter) } + .sorted() +// .map { it.format(inputFormatter) } + .forEachIndexed { index, label -> + options[label.format(inputFormatter)]?.let { + if (!it.isEmpty()) { + NavigationBarItem( + selected = currentTime == label.format(inputFormatter), + onClick = { + currentTime = label.format(inputFormatter) + }, + icon = { + Text( + text = label.format(outputFormatter), + modifier = Modifier + .testTag(TestIds.Book.ITEM_DATE) + ) + }, + modifier = Modifier + .testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + } + } + } + } + + LazyColumn( + Modifier + .fillMaxWidth() + ) { + options[currentTime]?.forEach { book -> + item { + Row( + Modifier + .fillMaxWidth() + .selectableGroup() + .testTag(TestIds.Book.getIdPlaceItemByPosition(book.id)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = book.place, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + RadioButton( + selected = book.id == selectedBooking && currentTime == selectedTime, + onClick = { + selectedBooking = book.id + selectedTime = currentTime + }, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) + ) + } + } + } + } Button( onClick = { - viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedBooking.value!!)) + viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedTime!!, selectedBooking!!)) }, modifier = Modifier .testTag(TestIds.Book.BOOK_BUTTON), - enabled = selectedBooking.value != null + enabled = selectedBooking != null ) { Text(stringResource(R.string.to_book)) } @@ -145,64 +216,5 @@ fun BookScreen( } -@Composable -fun TabGroup(options: Set) { - NavigationBar( - Modifier.fillMaxWidth() - ) { - options.forEachIndexed { index, label -> - NavigationBarItem( - selected = currentTime.value == label, - onClick = { - currentTime.value = label - }, - icon = { - Text( - text = label, - modifier = Modifier - .testTag(TestIds.Book.ITEM_DATE) - ) - }, - modifier = Modifier - .testTag(TestIds.Book.getIdDateItemByPosition(index)) - ) - } - } -} -@Composable -fun SelectBooking(options: List) { - LazyColumn( - Modifier - .fillMaxWidth() - ) { - options.forEachIndexed { index, book -> - item { - Row( - Modifier - .fillMaxWidth() - .selectableGroup() - .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = book.place, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_TEXT) - ) - RadioButton( - selected = book == selectedBooking.value && currentTime.value == selectedTime.value, - onClick = { - selectedBooking.value = book - selectedTime.value = currentTime.value - }, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) - ) - } - } - } - } -} 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 1b32a2f..6692699 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,11 +1,12 @@ package ru.myitschool.work.ui.screen.book -import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.ui.screen.BookingItem sealed interface BookState { object Loading: BookState data class Data( - val booking : Map> = mapOf() + val booking: Map> = mapOf() ): BookState data class Error( val error : String 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 841bb54..6e3b15a 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 @@ -14,13 +14,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.BookRepository -import ru.myitschool.work.ui.screen.Booking class BookViewModel : ViewModel() { private val _uiState = MutableStateFlow(BookState.Loading) val uiState: StateFlow = _uiState.asStateFlow(); - private val _navigationFlow = MutableSharedFlow() - val navigationFlow: SharedFlow = _navigationFlow.asSharedFlow() + private val _navigationFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) + val navigationFlow: SharedFlow = _navigationFlow init { loadData() @@ -61,9 +60,10 @@ class BookViewModel : ViewModel() { onSuccess = { it.let { bookings -> _uiState.update { - when (bookings.isEmpty()) { + Log.d("test", bookings.isEmpty().toString()) + when (!bookings.isEmpty()) { true -> BookState.Data( - booking = bookings.toMap() + booking = bookings ) false -> BookState.NotData } @@ -84,14 +84,18 @@ class BookViewModel : ViewModel() { fun onIntent(intent: BookIntent) { when (intent) { is BookIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { _uiState.update { BookState.Loading } - BookRepository.sendData(intent.code, intent.booking).fold( + BookRepository.sendData(intent.code, intent.data, intent.placeId).fold( onSuccess = { - _navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain) + Log.d("send date", "success") + _navigationFlow.emit(BookNavigationEvent.NavigateToMain) }, onFailure = { error -> - BookState.Error(error.message ?: "Неизвестная ошибка") + Log.d("send date", "error: $error") + _uiState.update { + BookState.Error(error.message ?: "Неизвестная ошибка") + } } ) } @@ -111,15 +115,6 @@ class BookViewModel : ViewModel() { } } -fun List.toMap() : Map> { - val options = this - val map : MutableMap> = mutableMapOf() - options.forEach { - if (map[it.date] == null) map[it.date] = mutableListOf(it) - else map[it.date]?.add(it) - } - return map -} sealed interface BookNavigationEvent { object NavigateToAuth : BookNavigationEvent 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 new file mode 100644 index 0000000..d7b99f3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainIntent { + data object LoadData : MainIntent + data object Logout : MainIntent + data object AddBooking : 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 new file mode 100644 index 0000000..507ac27 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,223 @@ +package ru.myitschool.work.ui.screen.main + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +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.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import coil3.compose.AsyncImage +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.screen.Booking + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + LaunchedEffect(state) { + Log.d("MainScreen", "UI State: $state") + } + LaunchedEffect(viewModel) { + viewModel.navigationFlow.collect { event -> + when (event) { + MainNavigationEvent.NavigateToAuth -> { + navController.navigate(AuthScreenDestination) { + popUpTo(navController.graph.startDestinationId) { inclusive = true } + } + } + MainNavigationEvent.NavigateToBook -> { + navController.navigate(BookScreenDestination) + } + } + } + } + + when (state) { + MainState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) + } + is MainState.Content -> { + Content( + state = state as MainState.Content, + onRefresh = { viewModel.onIntent(MainIntent.LoadData) }, + onLogout = { viewModel.onIntent(MainIntent.Logout) }, + onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) } + ) + } + is MainState.ErrorOnly -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = (state as MainState.ErrorOnly).message, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { viewModel.onIntent(MainIntent.LoadData) }, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + } + } + } +} + +@Composable +private fun Content( + state: MainState.Content, + onRefresh: () -> Unit, + onLogout: () -> Unit, + onAddBooking: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + state.userPhotoUrl?.let { url -> + AsyncImage( + model = url, + contentDescription = "Аватар", + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + } + state.userName?.let { + Text( + text = it, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button( + onClick = onLogout, + modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) + ) { + Text("Выйти") + } + Button( + onClick = onRefresh, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + Button( + onClick = onAddBooking, + modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) + ) { + Text("Забронировать") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + if (state.error != null) { + Text( + text = state.error, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) + Spacer(modifier = Modifier.height(8.dp)) + } + Text( + text = "Бронирования", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .fillMaxWidth() + .testTag("main_bookings_title") + ) + + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Дата", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.testTag("main_bookings_header_date") + ) + Text( + text = "Место", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.testTag("main_bookings_header_place") + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + LazyColumn(modifier = Modifier.fillMaxSize()) { + itemsIndexed(state.bookings) { index, item -> + BookingItemView(booking = item, index = index) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} +@Composable +private fun BookingItemView(booking: Booking, index: Int) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.getIdItemByPosition(index)) + .padding(8.dp) + ) { + Text( + text = booking.date, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = booking.place, + modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) + ) + } +} \ 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 new file mode 100644 index 0000000..29e8434 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.ui.screen.main + +import ru.myitschool.work.ui.screen.Booking + +sealed interface MainState { + object Loading : MainState + data class Content( + val userName: String?, + val userPhotoUrl: String?, + val bookings: List, + val error: String? = null + ) : MainState + data class ErrorOnly( + val message: String + ) : MainState +} \ No newline at end of file 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 new file mode 100644 index 0000000..842ddfb --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,102 @@ +package ru.myitschool.work.ui.screen.main + +import android.util.Log +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.launch +import kotlinx.coroutines.withContext +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.ui.screen.Booking +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class MainViewModel : ViewModel() { + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _navigationFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) + val navigationFlow: SharedFlow = _navigationFlow + fun formatDateString(isoDate: String): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + val date = LocalDate.parse(isoDate, inputFormatter) + return date.format(outputFormatter) + } + init { + loadData() + } + + private fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + _uiState.value = MainState.Loading + } + + val code = AuthRepository.getSavedCode() ?: run { + _navigationFlow.emit(MainNavigationEvent.NavigateToAuth) + return@launch + } + + MainRepository.loadUserInfo(code).fold( + onSuccess = { userInfo -> + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + val sortedBookings = userInfo.bookingToList() + .sortedBy { LocalDate.parse(it.date, inputFormatter) } + .map { booking -> + Booking( + date = LocalDate.parse(booking.date, inputFormatter).format(outputFormatter), + place = booking.place + ) + } + + withContext(Dispatchers.Main) { + _uiState.value = MainState.Content( + userName = userInfo.name, + userPhotoUrl = userInfo.photoUrl ?: "", + bookings = sortedBookings, + error = null + ) + } + }, + onFailure = { error -> + Log.e("MainViewModel", "Ошибка загрузки", error) + withContext(Dispatchers.Main) { + _uiState.value = MainState.ErrorOnly( + error.message ?: "Не удалось загрузить данные" + ) + } + } + ) + } + } + + fun onIntent(intent: MainIntent) { + when (intent) { + MainIntent.LoadData -> loadData() + MainIntent.Logout -> { + MainRepository.clearAuth() + viewModelScope.launch { + _navigationFlow.emit(MainNavigationEvent.NavigateToAuth) + } + } + MainIntent.AddBooking -> { + viewModelScope.launch { + _navigationFlow.emit(MainNavigationEvent.NavigateToBook) + } + } + } + } +} +sealed interface MainNavigationEvent { + object NavigateToAuth : MainNavigationEvent + object NavigateToBook : MainNavigationEvent +} \ No newline at end of file -- 2.34.1 From b11e010cdc919277ceaf8310d846c173facd91ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 8 Dec 2025 22:07:43 +0300 Subject: [PATCH 6/6] completed user authorization with a saved user, and also optimized imports --- .../main/java/ru/myitschool/work/data/repo/MainRepository.kt | 2 +- .../java/ru/myitschool/work/data/source/NetworkDataSource.kt | 4 ---- .../java/ru/myitschool/work/ui/screen/NavigationGraph.kt | 5 ----- .../java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt | 4 +++- .../main/java/ru/myitschool/work/ui/screen/book/BookState.kt | 1 - .../java/ru/myitschool/work/ui/screen/book/BookViewModel.kt | 1 - 6 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt index f768094..aff98a4 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -1,8 +1,8 @@ package ru.myitschool.work.data.repo +import androidx.core.content.edit import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.ui.screen.UserInfo -import androidx.core.content.edit object MainRepository { suspend fun loadUserInfo(code: String): Result { 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 aee6910..cea8a13 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 @@ -16,10 +16,8 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants -import ru.myitschool.work.ui.screen.Booking import ru.myitschool.work.ui.screen.BookingItem import ru.myitschool.work.ui.screen.CreateBookingRequest import ru.myitschool.work.ui.screen.UserInfo @@ -66,8 +64,6 @@ object NetworkDataSource { } - - suspend fun getFreeBooking(code: String): Result>> = withContext(Dispatchers.IO) { runCatching { 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 479c17c..c662bb9 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 @@ -3,10 +3,7 @@ package ru.myitschool.work.ui.screen import android.util.Log 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.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController @@ -18,9 +15,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 -import ru.myitschool.work.ui.screen.main.MainViewModel @Composable fun AppNavHost( 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 e22a5e4..d77ac73 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 @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column 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.size import androidx.compose.material3.Button @@ -30,6 +29,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.myitschool.work.R import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.MainScreenDestination @Composable @@ -45,6 +45,8 @@ fun AuthScreen( } } + AuthRepository.getSavedCode()?.let { viewModel.onIntent(AuthIntent.Send(it)) } + Column( modifier = Modifier .fillMaxSize() 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 6692699..ff8c214 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,6 +1,5 @@ package ru.myitschool.work.ui.screen.book -import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.ui.screen.BookingItem sealed interface BookState { 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 6e3b15a..787127d 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 @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -- 2.34.1