From 783f25ced664755812ad3894acf48cd015f02ce2 Mon Sep 17 00:00:00 2001 From: githubchikov Date: Sat, 29 Nov 2025 22:36:51 +0300 Subject: [PATCH 1/8] feat: add templates of screens --- .../work/ui/screen/NavigationGraph.kt | 14 ++---- .../work/ui/screen/auth/AuthScreen.kt | 11 +++-- .../work/ui/screen/book/BookIntent.kt | 6 +++ .../work/ui/screen/book/BookScreen.kt | 49 +++++++++++++++++++ .../work/ui/screen/book/BookState.kt | 5 ++ .../work/ui/screen/book/BookViewModel.kt | 39 +++++++++++++++ .../work/ui/screen/main/MainIntent.kt | 6 +++ .../work/ui/screen/main/MainScreen.kt | 49 +++++++++++++++++++ .../work/ui/screen/main/MainState.kt | 5 ++ .../work/ui/screen/main/MainViewModel.kt | 43 ++++++++++++++++ 10 files changed, 212 insertions(+), 15 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 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/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 01b0f32..7890fab 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,8 @@ 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 +import ru.myitschool.work.ui.screen.main.MainScreen @Composable fun AppNavHost( @@ -32,18 +34,10 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen(navController = navController) } 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/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index f99978e..04a269d 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 @@ -52,11 +52,6 @@ fun AuthScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Text( - text = stringResource(R.string.auth_title), - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) when (val currentState = state) { is AuthState.Data -> Content(viewModel, currentState) is AuthState.Loading -> { @@ -74,6 +69,12 @@ private fun Content( state: AuthState.Data ) { var inputText by remember { mutableStateOf("") } + + Text( + text = stringResource(R.string.auth_title), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) Spacer(modifier = Modifier.size(16.dp)) TextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), 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..ea49392 --- /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 TextInput(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..c4af615 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,49 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.ui.nav.MainScreenDestination + +@Composable +fun BookScreen( + viewModel: BookViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Text( + text = "страница бронирования...", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + + } +} \ 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..020007e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookState { + object Data: 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..1456461 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,39 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.lifecycle.ViewModel +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 ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase + +class BookViewModel : ViewModel() { + private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } + private val _uiState = MutableStateFlow(BookState.Data) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + fun onIntent(intent: BookIntent) { +// when (intent) { +// is MainIntent.Send -> { +// viewModelScope.launch(Dispatchers.Default) { +// _uiState.update { MainState.Loading } +// checkAndSaveAuthCodeUseCase.invoke("9999").fold( +// onSuccess = { +// _actionFlow.emit(Unit) +// }, +// onFailure = { error -> +// error.printStackTrace() +// _actionFlow.emit(Unit) +// } +// ) +// } +// } +// is MainIntent.TextInput -> Unit +// } + } +} \ 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 new file mode 100644 index 0000000..30678a3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainIntent { + data class Send(val text: String): MainIntent + data class TextInput(val text: String): 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..4e6d034 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,49 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.ui.nav.MainScreenDestination + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Text( + text = "главная страница...", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + + } +} \ 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..67ec049 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainState { + object Data: 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..80d525e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,43 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase + +class MainViewModel : ViewModel() { + private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(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) { +// is MainIntent.Send -> { +// viewModelScope.launch(Dispatchers.Default) { +// _uiState.update { MainState.Loading } +// checkAndSaveAuthCodeUseCase.invoke("9999").fold( +// onSuccess = { +// _actionFlow.emit(Unit) +// }, +// onFailure = { error -> +// error.printStackTrace() +// _actionFlow.emit(Unit) +// } +// ) +// } +// } +// is MainIntent.TextInput -> Unit +// } + } +} \ No newline at end of file -- 2.34.1 From dbc4830418ddae1692269c89615f7a13b06bffe9 Mon Sep 17 00:00:00 2001 From: githubchikov Date: Sun, 30 Nov 2025 22:22:20 +0300 Subject: [PATCH 2/8] feat: edit auth screen --- .../work/data/source/NetworkDataSource.kt | 12 +++- .../work/ui/screen/auth/AuthScreen.kt | 67 ++++++++++++++++--- .../work/ui/screen/auth/AuthState.kt | 7 +- .../work/ui/screen/auth/AuthViewModel.kt | 28 +++++--- .../work/ui/screen/book/BookScreen.kt | 22 ++++++ .../work/ui/screen/main/MainScreen.kt | 26 +++++++ 6 files changed, 135 insertions(+), 27 deletions(-) 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..8772066 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -4,7 +4,6 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers @@ -29,14 +28,21 @@ object NetworkDataSource { } suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { - return@withContext runCatching { + runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) when (response.status) { HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + HttpStatusCode.Unauthorized -> error("Код не существует") + HttpStatusCode.BadRequest -> error("Что-то пошло не так") + else -> error("Неизвестная ошибка: ${response.status}") } + }.mapCatching { success -> + success + }.recoverCatching { _ -> + throw Exception("Не удалось соединиться с сервером") } } + 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/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 04a269d..1d78716 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 @@ -31,6 +31,12 @@ import androidx.navigation.NavController import ru.myitschool.work.R import ru.myitschool.work.core.TestIds import ru.myitschool.work.ui.nav.MainScreenDestination +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions @Composable fun AuthScreen( @@ -48,50 +54,89 @@ fun AuthScreen( Column( modifier = Modifier .fillMaxSize() - .padding(all = 24.dp), + .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) + when (state) { is AuthState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) + CircularProgressIndicator(modifier = Modifier.size(64.dp)) + } + + else -> { + Content(viewModel, state) } } } } + @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState ) { var inputText by remember { mutableStateOf("") } + val isButtonEnabled = + inputText.length == 4 && inputText.all { it.isLetterOrDigit() } + Text( text = stringResource(R.string.auth_title), style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) + Spacer(modifier = Modifier.size(16.dp)) + TextField( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Auth.CODE_INPUT), value = inputText, onValueChange = { - inputText = it + if (it.length <= 4 && it.all { ch -> ch.isLetterOrDigit() }) { + inputText = it + } viewModel.onIntent(AuthIntent.TextInput(it)) }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + viewModel.onIntent(AuthIntent.Send(inputText)) + } + ), + singleLine = true, label = { Text(stringResource(R.string.auth_label)) } ) + + Spacer(modifier = Modifier.size(12.dp)) + + AnimatedVisibility( + visible = state is AuthState.Error, + enter = fadeIn(), + exit = fadeOut() + ) { + Text( + text = (state as AuthState.Error).message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.size(16.dp)) + Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Auth.SIGN_BUTTON), onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, - enabled = true + enabled = isButtonEnabled ) { Text(stringResource(R.string.auth_sign_in)) } 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 a06ba76..a4c1793 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 @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.screen.auth -sealed interface AuthState { - object Loading: AuthState - object Data: AuthState +sealed class AuthState { + data object Data : AuthState() + data object Loading : AuthState() + data class Error(val message: String) : AuthState() } \ No newline at end of file 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 3153640..289896d 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 @@ -13,31 +13,39 @@ import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase -class AuthViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } +class AuthViewModel() : ViewModel() { + private val checkAndSaveAuthCodeUseCase by lazy { + CheckAndSaveAuthCodeUseCase(AuthRepository) + } + private val _uiState = MutableStateFlow(AuthState.Data) val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + private val _actionFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: AuthIntent) { when (intent) { + is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { - _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( + viewModelScope.launch(Dispatchers.IO) { + _uiState.value = AuthState.Loading + + checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { _actionFlow.emit(Unit) }, - onFailure = { error -> - error.printStackTrace() - _actionFlow.emit(Unit) + onFailure = { throwable -> + val errorMessage = throwable.message ?: "Неизвестная ошибка" + _uiState.value = AuthState.Error(errorMessage) } ) } } - is AuthIntent.TextInput -> Unit + + is AuthIntent.TextInput -> { + _uiState.value = AuthState.Data + } } } } \ 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 c4af615..6adc0ed 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 @@ -31,6 +31,28 @@ fun BookScreen( } } + /* + Экран бронирования + На данном экране необходимо вывести возможные даты и места для бронирования. + Элементы, которые должны присутствовать на экране: + Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. + В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: + Текстовое поле (book_place_text), в котором содержится место доступное для брони. + Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) + Кнопка (book_book_button) для бронирования. + Кнопка (book_back_button) для возвращения на предыдущий экран. + По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. + По умолчанию неотображаемая кнопка обновить (book_refresh_button). + По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + + Требования к компонентам: + По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). + Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. + Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. + В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. + При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. + */ + Column( modifier = Modifier .fillMaxSize() 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 4e6d034..a4a71c8 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 @@ -31,6 +31,32 @@ fun MainScreen( } } + /* + Главный экран + Данный экран содержит информацию о пользователе и его текущие бронирования. Если пользователь авторизован, данный экран должен быть отображен при запуске приложения. + + Элементы, которые должны присутствовать на экране: + + Текстовое поле (main_name), в котором написано имя пользователя. + Изображение (main_photo), на котором отображено фото пользователя. + Кнопка (main_logout_button) для выхода пользователя из аккаунта. + Кнопка (main_refresh_button) для принудительного обновления данных. + Кнопка (main_add_button) для бронирования. + Список, содержащий однотипные элементы (main_book_pos_{индекс}), со следующим содержимым: + Текстовое поле (main_item_date) с датой бронирования в формате dd.MM.yyyy. + Текстовое поле (main_item_place) с местом, которое забронировано. + По умолчанию скрытое текстовое поле с ошибкой (main_error). + Требования к компонентам: + + В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. + Для получения данных необходимо использовать сетевой запрос /api//info. + При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. + При нажатии кнопки бронирования необходимо открыть экран бронирования. + При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. + Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). + */ + + Column( modifier = Modifier .fillMaxSize() -- 2.34.1 From 72e8b946c00cdbbfdaeaa7d2dff7d70839336197 Mon Sep 17 00:00:00 2001 From: githubchikov Date: Mon, 1 Dec 2025 22:01:00 +0300 Subject: [PATCH 3/8] feat: edit main screen - add template texts --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 3 +- .../work/ui/screen/NavigationGraph.kt | 3 +- .../work/ui/screen/auth/AuthScreen.kt | 2 + .../work/ui/screen/main/MainScreen.kt | 221 +++++++++++++++--- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/themes.xml | 8 + 7 files changed, 212 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/values/themes.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..8b61370 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") @@ -48,4 +49,5 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktor") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("io.coil-kt:coil-compose:2.6.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2c02bd..0eb09d5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,10 +19,9 @@ android:name=".ui.root.RootActivity" android:exported="true" android:windowSoftInputMode="adjustResize" - android:label="@string/title_activity_root"> + android:theme="@style/Theme.Work"> - diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 7890fab..d843e79 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 @@ -28,7 +28,8 @@ fun AppNavHost( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, +// startDestination = AuthScreenDestination, + startDestination = MainScreenDestination, ) { composable { AuthScreen(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 1d78716..065a0c2 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 @@ -34,6 +34,7 @@ import ru.myitschool.work.ui.nav.MainScreenDestination import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.imePadding import androidx.compose.ui.text.input.ImeAction import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -54,6 +55,7 @@ fun AuthScreen( Column( modifier = Modifier .fillMaxSize() + .imePadding() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center 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 a4a71c8..0e9895d 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 @@ -2,20 +2,41 @@ package ru.myitschool.work.ui.screen.main 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.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import coil3.compose.AsyncImage +import ru.myitschool.work.R +import ru.myitschool.work.core.TestIds import ru.myitschool.work.ui.nav.MainScreenDestination @Composable @@ -25,27 +46,9 @@ fun MainScreen( ) { val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - /* - Главный экран - Данный экран содержит информацию о пользователе и его текущие бронирования. Если пользователь авторизован, данный экран должен быть отображен при запуске приложения. - - Элементы, которые должны присутствовать на экране: - - Текстовое поле (main_name), в котором написано имя пользователя. - Изображение (main_photo), на котором отображено фото пользователя. - Кнопка (main_logout_button) для выхода пользователя из аккаунта. - Кнопка (main_refresh_button) для принудительного обновления данных. - Кнопка (main_add_button) для бронирования. - Список, содержащий однотипные элементы (main_book_pos_{индекс}), со следующим содержимым: - Текстовое поле (main_item_date) с датой бронирования в формате dd.MM.yyyy. - Текстовое поле (main_item_place) с местом, которое забронировано. По умолчанию скрытое текстовое поле с ошибкой (main_error). + Требования к компонентам: В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. @@ -56,20 +59,184 @@ fun MainScreen( Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). */ + val bookings = listOf( + Booking(date = "2025-12-01", place = "Аудитория 1"), + Booking(date = "2025-12-01", place = "Аудитория 2"), + Booking(date = "2025-12-02", place = "Аудитория 3"), + Booking(date = "2025-12-02", place = "Конференц-зал"), + Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), + Booking(date = "2025-12-03", place = "Лаборатория №101"), + Booking(date = "2025-12-04", place = "Переговорная комната"), + Booking(date = "2025-12-04", place = "Спортивный зал"), + ) + + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { + navController.navigate(MainScreenDestination) + } + } Column( modifier = Modifier .fillMaxSize() - .padding(all = 24.dp), + .imePadding() + .padding( + start = 20.dp, + top = 20.dp, + end = 20.dp, + bottom = 0.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Top ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(60.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = "Иванов Иван Иванович", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .testTag(TestIds.Main.PROFILE_NAME) + ) + } - Text( - text = "главная страница...", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) + Spacer(modifier = Modifier.size(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.LOGOUT_BUTTON), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError + ), + onClick = { + }, + ) { + Text(stringResource(R.string.logout)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + onClick = { + }, + ) { + Text(stringResource(R.string.refresh)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.ADD_BUTTON), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF2E7D32), + contentColor = Color.White + ), + onClick = { + }, + ) { + Text(stringResource(R.string.book_new)) + } + + Scaffold( + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + itemsIndexed( + items = bookings, + key = { index, item -> "main_book_pos_$index" } + ) { index, booking -> + BookCard( + date = booking.date, + place = booking.place, + modifier = Modifier.padding( + top = if (index == 0) 0.dp else 8.dp, + bottom = if (index == bookings.lastIndex) 0.dp else 8.dp + ) + ) + } + } + } + } +} + +data class Booking( + val date: String, + val place: String +) + +@Composable +fun BookCard( + date: String, + place: String, + modifier: Modifier = Modifier +) { + val formattedDate = remember(date) { + try { + val parts = date.split("-") + if (parts.size == 3) { + "${parts[2]}.${parts[1]}.${parts[0]}" + } else { + date + } + } catch (_: Exception) { + date + } + } + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + modifier = modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "Бронь на $formattedDate", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .testTag(TestIds.Main.ITEM_DATE) + ) + + Spacer(modifier = Modifier.size(8.dp)) + + Text( + text = place, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .testTag(TestIds.Main.ITEM_PLACE) + ) + } } } \ 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..1a695a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,9 @@ Work - RootActivity Привет! Введи код для авторизации Код Войти + Выйти + Обновить данные + Новая бронь \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..07e5fc2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file -- 2.34.1 From 56c14cbac576dec5b89c8cc35c38d3f640e1eb5e Mon Sep 17 00:00:00 2001 From: CryptoDruid802 Date: Thu, 11 Dec 2025 21:53:38 +0300 Subject: [PATCH 4/8] new fun: exit --- .../java/ru/myitschool/work/data/repo/AuthRepository.kt | 4 ++++ .../java/ru/myitschool/work/ui/screen/main/MainScreen.kt | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) 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 3ef28f1..9ed569a 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 @@ -5,6 +5,10 @@ import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { private var codeCache: String? = null + fun clearCode() { + codeCache = null + } + suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 0e9895d..e2dd3ca 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 @@ -37,6 +37,8 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R 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 @Composable @@ -53,7 +55,7 @@ fun MainScreen( В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. Для получения данных необходимо использовать сетевой запрос /api//info. - При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. + При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. ГОТОВО При нажатии кнопки бронирования необходимо открыть экран бронирования. При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). @@ -122,6 +124,8 @@ fun MainScreen( contentColor = MaterialTheme.colorScheme.onError ), onClick = { + AuthRepository.clearCode() + navController.navigate(AuthScreenDestination) }, ) { Text(stringResource(R.string.logout)) -- 2.34.1 From caff70568d8dfd71e93cfae606cacb4e686243ab Mon Sep 17 00:00:00 2001 From: CryptoDruid802 Date: Thu, 11 Dec 2025 22:03:28 +0300 Subject: [PATCH 5/8] new fun: exit --- .../main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2dd3ca..9403122 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 @@ -55,7 +55,7 @@ fun MainScreen( В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. Для получения данных необходимо использовать сетевой запрос /api//info. - При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. ГОТОВО + При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации.ГОТОВО При нажатии кнопки бронирования необходимо открыть экран бронирования. При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). -- 2.34.1 From 9e6ef6062fe97e81a42449e9ab6983a089f36c5e Mon Sep 17 00:00:00 2001 From: githubchikov Date: Thu, 11 Dec 2025 22:14:19 +0300 Subject: [PATCH 6/8] feat: add query's to backend --- app/build.gradle.kts | 3 + .../work/data/source/NetworkDataSource.kt | 16 +- .../work/ui/screen/NavigationGraph.kt | 19 +- .../work/ui/screen/auth/AuthScreen.kt | 14 +- .../work/ui/screen/book/BookIntent.kt | 10 +- .../work/ui/screen/book/BookScreen.kt | 221 +++++++++++--- .../work/ui/screen/book/BookState.kt | 16 +- .../work/ui/screen/book/BookViewModel.kt | 148 +++++++-- .../work/ui/screen/main/MainScreen.kt | 287 +++++++++--------- .../work/ui/screen/main/MainState.kt | 4 +- 10 files changed, 513 insertions(+), 225 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b61370..57eaa73 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,9 @@ android { dependencies { implementation("androidx.compose.material3:material3:1.4.0") + implementation("androidx.compose.runtime:runtime:1.10.0") + implementation("androidx.compose.foundation:foundation-layout:1.10.0") + implementation("androidx.compose.foundation:foundation:1.10.0") defaultComposeLibrary() implementation("androidx.datastore:datastore-preferences:1.1.7") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 8772066..da6c74f 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -4,6 +4,7 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers @@ -28,8 +29,16 @@ object NetworkDataSource { } suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { + val url = getUrl(code, Constants.AUTH_URL) + + println("➡ Request URL: $url") + runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) + val response = client.get(url) + + println("⬅ Response status: ${response.status}") + println("⬅ Response body: ${response.bodyAsText()}") + when (response.status) { HttpStatusCode.OK -> true HttpStatusCode.Unauthorized -> error("Код не существует") @@ -38,8 +47,9 @@ object NetworkDataSource { } }.mapCatching { success -> success - }.recoverCatching { _ -> - throw Exception("Не удалось соединиться с сервером") + }.recoverCatching { e -> + println("❌ Error: ${e.message}") + throw Exception(e.message) } } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index d843e79..34e7d6b 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -16,6 +17,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.book.BookScreen +import ru.myitschool.work.ui.screen.book.BookViewModel import ru.myitschool.work.ui.screen.main.MainScreen @Composable @@ -28,8 +30,8 @@ fun AppNavHost( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, -// startDestination = AuthScreenDestination, - startDestination = MainScreenDestination, + startDestination = AuthScreenDestination, +// startDestination = MainScreenDestination, ) { composable { AuthScreen(navController = navController) @@ -38,7 +40,18 @@ fun AppNavHost( MainScreen(navController = navController) } composable { - BookScreen(navController = navController) + val vm: BookViewModel = viewModel() + + BookScreen( + vm = vm, + onBack = { navController.popBackStack() }, + onSuccess = { + navController.popBackStack() + navController.navigate(MainScreenDestination) { + launchSingleTop = true + } + } + ) } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 065a0c2..4e5eaf1 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -121,12 +121,14 @@ private fun Content( enter = fadeIn(), exit = fadeOut() ) { - Text( - text = (state as AuthState.Error).message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) + (state as? AuthState.Error)?.let { errorState -> + Text( + text = errorState.message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } } Spacer(modifier = Modifier.size(16.dp)) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt index ea49392..55a6a3c 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -1,6 +1,12 @@ package ru.myitschool.work.ui.screen.book +import java.time.LocalDate + sealed interface BookIntent { - data class Send(val text: String): BookIntent - data class TextInput(val text: String): BookIntent + object Load : BookIntent + data class SelectDate(val date: LocalDate) : BookIntent + data class SelectPlace(val placeId: String) : BookIntent + object ConfirmBooking : BookIntent + object Refresh : BookIntent + object Back : BookIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 6adc0ed..2043de2 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -5,67 +5,208 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.myitschool.work.ui.nav.MainScreenDestination +/* +Экран бронирования +На данном экране необходимо вывести возможные даты и места для бронирования. + +Элементы, которые должны присутствовать на экране: +Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. + +В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: + Текстовое поле (book_place_text), в котором содержится место доступное для брони. + Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) + +Кнопка (book_book_button) для бронирования. +Кнопка (book_back_button) для возвращения на предыдущий экран. +По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. +По умолчанию неотображаемая кнопка обновить (book_refresh_button). +По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + +Требования к компонентам: +По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). +Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. +Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. +В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. +При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. + */ + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.Composable +import androidx.compose.ui.draw.clip +import ru.myitschool.work.ui.screen.auth.AuthIntent +import java.time.format.DateTimeFormatter + @Composable fun BookScreen( - viewModel: BookViewModel = viewModel(), - navController: NavController + vm: BookViewModel, + onBack: () -> Unit, + onSuccess: () -> Unit ) { - val state by viewModel.uiState.collectAsState() + val state by vm.state.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + vm.accept(BookIntent.Load) + } + + LaunchedEffect(state.bookingSuccess) { + if (state.bookingSuccess) { + onSuccess() } } - /* - Экран бронирования - На данном экране необходимо вывести возможные даты и места для бронирования. - Элементы, которые должны присутствовать на экране: - Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM. - В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит: - Текстовое поле (book_place_text), в котором содержится место доступное для брони. - Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable) - Кнопка (book_book_button) для бронирования. - Кнопка (book_back_button) для возвращения на предыдущий экран. - По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться. - По умолчанию неотображаемая кнопка обновить (book_refresh_button). - По умолчанию неотображаемый текст “Всё забронировано” (book_empty). + when { + state.loading -> { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } - Требования к компонентам: - По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января"). - Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать. - Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7. - В случае ошибки при получении данных о доступном бронировании в запросе api//booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные. - При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его. - */ - - Column( - modifier = Modifier - .fillMaxSize() - .padding(all = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - - Text( - text = "страница бронирования...", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center + state.error -> ErrorBlock( + onRefresh = { vm.accept(BookIntent.Refresh) }, + onBack = { onBack() } ) + state.isEmpty -> EmptyBlock( + onBack = { onBack() } + ) + + else -> ContentBlock( + state = state, + onDateClick = { vm.accept(BookIntent.SelectDate(it)) }, + onPlaceClick = { vm.accept(BookIntent.SelectPlace(it)) }, + onConfirm = { vm.accept(BookIntent.ConfirmBooking) }, + onBack = { onBack() } + ) + } +} + +@Composable +private fun ContentBlock( + state: BookState, + onDateClick: (java.time.LocalDate) -> Unit, + onPlaceClick: (String) -> Unit, + onConfirm: () -> Unit, + onBack: () -> Unit +) { + val f = DateTimeFormatter.ofPattern("dd.MM") + + Column(Modifier.fillMaxSize().padding(16.dp)) { + + PrimaryScrollableTabRow( + selectedTabIndex = state.dates.indexOfFirst { it.date == state.selectedDate } + ) { + state.dates.forEachIndexed { index, item -> + Tab( + selected = state.selectedDate == item.date, + onClick = { onDateClick(item.date) }, + text = { Text(item.date.format(f)) } + ) + } + } + + Spacer(Modifier.height(16.dp)) + + // группа единственного выбора + Column(Modifier.selectableGroup()) { + state.availablePlaces.forEach { place -> + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .selectable( + selected = state.selectedPlaceId == place.id, + onClick = { onPlaceClick(place.id) } + ) + .padding(horizontal = 8.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 0.dp), + text = place.title + ) + + RadioButton( + selected = state.selectedPlaceId == place.id, + onClick = { onPlaceClick(place.id) } + ) + } + } + } + + Spacer(Modifier.height(24.dp)) + + Button( + enabled = state.selectedPlaceId != null, + onClick = onConfirm, + modifier = Modifier.fillMaxWidth() + ) { + Text("Бронировать") + } + + Spacer(Modifier.height(12.dp)) + + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } + } +} + + +@Composable +fun ErrorBlock(onRefresh: () -> Unit, onBack: () -> Unit) { + Column( + Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center + ) { + Text("Ошибка загрузки", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(12.dp)) + Button(onClick = onRefresh, modifier = Modifier.fillMaxWidth()) { + Text("Обновить") + } + Spacer(Modifier.height(12.dp)) + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } + } +} + +@Composable +fun EmptyBlock(onBack: () -> Unit) { + Column( + Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center + ) { + Text("Всё забронировано", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(16.dp)) + TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { + Text("Назад") + } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt index 020007e..e0c3e9e 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -1,5 +1,17 @@ package ru.myitschool.work.ui.screen.book +import java.time.LocalDate -sealed interface BookState { - object Data: BookState +data class BookState( + val loading: Boolean = true, + val error: Boolean = false, + val dates: List = emptyList(), + val selectedDate: LocalDate? = null, + val selectedPlaceId: String? = null, + val bookingSuccess: Boolean = false +) { + val availablePlaces: List + get() = dates.firstOrNull { it.date == selectedDate }?.places ?: emptyList() + + val isEmpty: Boolean + get() = dates.isEmpty() } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index 1456461..2128950 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -1,39 +1,141 @@ package ru.myitschool.work.ui.screen.book import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase +import java.time.LocalDate +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class BookingDate( + val date: LocalDate, + val places: List +) + +data class BookingPlace( + val id: String, + val title: String +) + class BookViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(BookState.Data) - val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _state = MutableStateFlow(BookState()) + val state = _state.asStateFlow() - fun onIntent(intent: BookIntent) { -// when (intent) { -// is MainIntent.Send -> { -// viewModelScope.launch(Dispatchers.Default) { -// _uiState.update { MainState.Loading } -// checkAndSaveAuthCodeUseCase.invoke("9999").fold( -// onSuccess = { -// _actionFlow.emit(Unit) -// }, -// onFailure = { error -> -// error.printStackTrace() -// _actionFlow.emit(Unit) -// } -// ) -// } -// } -// is MainIntent.TextInput -> Unit -// } + fun accept(intent: BookIntent) { + when (intent) { + is BookIntent.Load -> load() + is BookIntent.SelectDate -> selectDate(intent.date) + is BookIntent.SelectPlace -> selectPlace(intent.placeId) + is BookIntent.ConfirmBooking -> book() + is BookIntent.Refresh -> load() + is BookIntent.Back -> {} // обработка навигации снаружи + } + } + + private fun load() { + viewModelScope.launch { + _state.value = BookState(loading = true) + + delay(400) // имитация ожидания API + + val result = fakeApi() + + if (result.isEmpty()) { + _state.value = BookState( + dates = emptyList(), + loading = false + ) + return@launch + } + + val earliest = result.minBy { it.date } + + _state.value = BookState( + dates = result, + selectedDate = earliest.date, + loading = false + ) + } + } + + private fun selectDate(date: LocalDate) { + _state.value = _state.value.copy( + selectedDate = date, + selectedPlaceId = null + ) + } + + private fun selectPlace(placeId: String) { + _state.value = _state.value.copy(selectedPlaceId = placeId) + } + + private fun book() { + viewModelScope.launch { + _state.value = _state.value.copy(loading = true) + + delay(300) // фейковое бронирование + + _state.value = _state.value.copy( + bookingSuccess = true, + loading = false + ) + } + } + + private fun fakeApi(): List { + return listOf( + BookingDate( + LocalDate.of(2025, 1, 5), + listOf( + BookingPlace("1", "Окно №1"), + BookingPlace("2", "Окно №3") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 6), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 9), + emptyList() // будет скрыто + ), + BookingDate( + LocalDate.of(2025, 1, 10), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 11), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 12), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + BookingDate( + LocalDate.of(2025, 1, 13), + listOf( + BookingPlace("3", "Окно №2") + ) + ), + ).filter { it.places.isNotEmpty() } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 0e9895d..998160c 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -16,36 +16,40 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R import ru.myitschool.work.core.TestIds -import ru.myitschool.work.ui.nav.MainScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination + +data class Booking( + val date: String, + val place: String +) @Composable -fun MainScreen( - viewModel: MainViewModel = viewModel(), - navController: NavController +fun BookCard( + date: String, + place: String, + modifier: Modifier = Modifier ) { - val state by viewModel.uiState.collectAsState() - /* По умолчанию скрытое текстовое поле с ошибкой (main_error). @@ -59,143 +63,6 @@ fun MainScreen( Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). */ - val bookings = listOf( - Booking(date = "2025-12-01", place = "Аудитория 1"), - Booking(date = "2025-12-01", place = "Аудитория 2"), - Booking(date = "2025-12-02", place = "Аудитория 3"), - Booking(date = "2025-12-02", place = "Конференц-зал"), - Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), - Booking(date = "2025-12-03", place = "Лаборатория №101"), - Booking(date = "2025-12-04", place = "Переговорная комната"), - Booking(date = "2025-12-04", place = "Спортивный зал"), - ) - - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding( - start = 20.dp, - top = 20.dp, - end = 20.dp, - bottom = 0.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(60.dp) - .clip(CircleShape) - .testTag(TestIds.Main.PROFILE_IMAGE) - ) - Spacer(modifier = Modifier.size(16.dp)) - Text( - text = "Иванов Иван Иванович", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .testTag(TestIds.Main.PROFILE_NAME) - ) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.LOGOUT_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError - ), - onClick = { - }, - ) { - Text(stringResource(R.string.logout)) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ), - onClick = { - }, - ) { - Text(stringResource(R.string.refresh)) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.ADD_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFF2E7D32), - contentColor = Color.White - ), - onClick = { - }, - ) { - Text(stringResource(R.string.book_new)) - } - - Scaffold( - modifier = Modifier.fillMaxSize() - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - itemsIndexed( - items = bookings, - key = { index, item -> "main_book_pos_$index" } - ) { index, booking -> - BookCard( - date = booking.date, - place = booking.place, - modifier = Modifier.padding( - top = if (index == 0) 0.dp else 8.dp, - bottom = if (index == bookings.lastIndex) 0.dp else 8.dp - ) - ) - } - } - } - } -} - -data class Booking( - val date: String, - val place: String -) - -@Composable -fun BookCard( - date: String, - place: String, - modifier: Modifier = Modifier -) { val formattedDate = remember(date) { try { val parts = date.split("-") @@ -239,4 +106,134 @@ fun BookCard( ) } } +} + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = { /* обновить данные */ } + ) { + Text(stringResource(R.string.refresh)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + when (state) { + is MainState.Error -> { + Text( + text = (state as MainState.Error).message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + is MainState.Loading -> { + CircularProgressIndicator() + } + is MainState.Data -> { + MainContent(navController = navController) + } + } + } +} + +@Composable +fun MainContent(navController: NavController) { + val bookings = listOf( + Booking(date = "2025-12-01", place = "Аудитория 1"), + Booking(date = "2025-12-01", place = "Аудитория 2"), + Booking(date = "2025-12-02", place = "Аудитория 3"), + Booking(date = "2025-12-02", place = "Конференц-зал"), + Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), + Booking(date = "2025-12-03", place = "Лаборатория №101"), + Booking(date = "2025-12-04", place = "Переговорная комната"), + Booking(date = "2025-12-04", place = "Спортивный зал"), + ) + + Column( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(60.dp) + .clip(CircleShape) + ) + Spacer(modifier = Modifier.size(16.dp)) + Text( + text = "Иванов Иван Иванович", + style = MaterialTheme.typography.titleLarge + ) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ), + onClick = { /* выход */ } + ) { + Text(stringResource(R.string.logout)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { navController.navigate(BookScreenDestination) } + ) { + Text(stringResource(R.string.book_new)) + } + + Spacer(modifier = Modifier.size(8.dp)) + + Scaffold( + modifier = Modifier.fillMaxSize() + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + itemsIndexed( + items = bookings, + key = { index, item -> "main_book_pos_$index" } + ) { index, booking -> + BookCard( + date = booking.date, + place = booking.place, + modifier = Modifier.padding( + top = if (index == 0) 0.dp else 4.dp, + bottom = if (index == bookings.lastIndex) 0.dp else 4.dp + ) + ) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt index 67ec049..680d44a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -1,5 +1,7 @@ package ru.myitschool.work.ui.screen.main sealed interface MainState { - object Data: MainState + data object Data : MainState + data object Loading : MainState + data class Error(val message: String) : MainState } \ No newline at end of file -- 2.34.1 From e5df83f4e316177aeec74ab0cced8b856b354bcc Mon Sep 17 00:00:00 2001 From: githubchikov Date: Thu, 11 Dec 2025 22:33:57 +0300 Subject: [PATCH 7/8] fix: fix main screen --- .../work/ui/screen/main/MainScreen.kt | 151 +++++++++--------- 1 file changed, 72 insertions(+), 79 deletions(-) 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 1cd5afd..7d4fd94 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -16,30 +16,45 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R import ru.myitschool.work.core.TestIds -import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination + + +/* +По умолчанию скрытое текстовое поле с ошибкой (main_error). + +Требования к компонентам: + +В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. +Для получения данных необходимо использовать сетевой запрос /api//info. +При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. +При нажатии кнопки бронирования необходимо открыть экран бронирования. +При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. +Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). + */ + @Composable fun MainScreen( @@ -48,19 +63,46 @@ fun MainScreen( ) { val state by viewModel.uiState.collectAsState() - /* - По умолчанию скрытое текстовое поле с ошибкой (main_error). + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = { /* обновить данные */ } + ) { + Text(stringResource(R.string.refresh)) + } - Требования к компонентам: + Spacer(modifier = Modifier.size(8.dp)) - В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. - Для получения данных необходимо использовать сетевой запрос /api//info. - При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. ГОТОВО - При нажатии кнопки бронирования необходимо открыть экран бронирования. - При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. - Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). - */ + when (state) { + is MainState.Error -> { + Text( + text = (state as MainState.Error).message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + is MainState.Loading -> { + CircularProgressIndicator() + } + is MainState.Data -> { + MainContent(navController = navController) + } + } + } +} +@Composable +fun MainContent(navController: NavController) { val bookings = listOf( Booking(date = "2025-12-01", place = "Аудитория 1"), Booking(date = "2025-12-01", place = "Аудитория 2"), @@ -72,23 +114,8 @@ fun MainScreen( Booking(date = "2025-12-04", place = "Спортивный зал"), ) - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } - } - Column( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding( - start = 20.dp, - top = 20.dp, - end = 20.dp, - bottom = 0.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top + modifier = Modifier.fillMaxSize() ) { Row( modifier = Modifier.fillMaxWidth(), @@ -102,26 +129,20 @@ fun MainScreen( modifier = Modifier .size(60.dp) .clip(CircleShape) - .testTag(TestIds.Main.PROFILE_IMAGE) ) Spacer(modifier = Modifier.size(16.dp)) Text( text = "Иванов Иван Иванович", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .testTag(TestIds.Main.PROFILE_NAME) + style = MaterialTheme.typography.titleLarge ) } Spacer(modifier = Modifier.size(8.dp)) Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.LOGOUT_BUTTON), + modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError + containerColor = MaterialTheme.colorScheme.error ), onClick = { AuthRepository.clearCode() @@ -134,35 +155,14 @@ fun MainScreen( Spacer(modifier = Modifier.size(8.dp)) Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary - ), - onClick = { - }, - ) { - Text(stringResource(R.string.refresh)) - } - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.ADD_BUTTON), - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFF2E7D32), - contentColor = Color.White - ), - onClick = { - }, + modifier = Modifier.fillMaxWidth(), + onClick = { navController.navigate(BookScreenDestination) } ) { Text(stringResource(R.string.book_new)) } + Spacer(modifier = Modifier.size(8.dp)) + Scaffold( modifier = Modifier.fillMaxSize() ) { paddingValues -> @@ -189,11 +189,6 @@ fun MainScreen( } } -data class Booking( - val date: String, - val place: String -) - @Composable fun BookCard( date: String, @@ -201,16 +196,9 @@ fun BookCard( modifier: Modifier = Modifier ) { val formattedDate = remember(date) { - try { - val parts = date.split("-") - if (parts.size == 3) { - "${parts[2]}.${parts[1]}.${parts[0]}" - } else { - date - } - } catch (_: Exception) { - date - } + runCatching { + java.time.LocalDate.parse(date).format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")) + }.getOrElse { date } } Card( @@ -243,4 +231,9 @@ fun BookCard( ) } } -} \ No newline at end of file +} + +data class Booking( + val date: String, + val place: String +) \ No newline at end of file -- 2.34.1 From 08f40f72caa7eee3cca59c677f2fe08a0d2ef544 Mon Sep 17 00:00:00 2001 From: CryptoDruid802 Date: Fri, 12 Dec 2025 00:10:08 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20+=20?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/models/BookingInfo.kt | 9 ++ .../myitschool/work/data/models/UserInfo.kt | 10 +++ .../work/data/repo/AuthRepository.kt | 4 + .../work/data/source/NetworkDataSource.kt | 45 ++++++++-- .../work/domain/GetUserInfoUseCase.kt | 11 +++ .../work/ui/screen/main/MainScreen.kt | 85 +++++++++---------- .../work/ui/screen/main/MainState.kt | 8 +- .../work/ui/screen/main/MainViewModel.kt | 54 +++++------- 9 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.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..13f7e39 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://192.168.0.111: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/models/BookingInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt new file mode 100644 index 0000000..d915ec1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/BookingInfo.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class BookingInfo( + val id: Int, + val place: String +) diff --git a/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt new file mode 100644 index 0000000..699b399 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/UserInfo.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val name: String, + val photoUrl: String, + val booking: Map +) 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 9ed569a..57bbc44 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 @@ -9,6 +9,10 @@ object AuthRepository { codeCache = null } + fun getCode(): String? { + return codeCache + } + suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> 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 da6c74f..a4afdc5 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 @@ -11,23 +11,50 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.data.models.UserInfo object NetworkDataSource { + private val json = Json { + isLenient = true + ignoreUnknownKeys = true + explicitNulls = true + encodeDefaults = true + } + private val client by lazy { HttpClient(CIO) { install(ContentNegotiation) { - json( - Json { - isLenient = true - ignoreUnknownKeys = true - explicitNulls = true - encodeDefaults = true - } - ) + json(json) } } } + suspend fun getUserInfo(code: String): Result = withContext(Dispatchers.IO) { + val url = getUrl(code, "/info") + + println("➡ Request URL: $url") + + runCatching { + val response = client.get(url) + + println("⬅ Response status: ${response.status}") + + when (response.status) { + HttpStatusCode.OK -> { + val body = response.bodyAsText() + println("⬅ Response body: $body") + json.decodeFromString(body) + } + HttpStatusCode.Unauthorized -> error("Код не существует") + HttpStatusCode.BadRequest -> error("Что-то пошло не так") + else -> error("Неизвестная ошибка: ${response.status}") + } + }.recoverCatching { e -> + println("❌ Error: ${e.message}") + throw Exception(e.message) + } + } + suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { val url = getUrl(code, Constants.AUTH_URL) @@ -55,4 +82,4 @@ object NetworkDataSource { 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/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.kt new file mode 100644 index 0000000..536c16d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/GetUserInfoUseCase.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.domain + +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.source.NetworkDataSource + +class GetUserInfoUseCase { + suspend operator fun invoke() = runCatching { + val code = AuthRepository.getCode() ?: error("Вы не авторизованы") + NetworkDataSource.getUserInfo(code).getOrThrow() + } +} 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 7d4fd94..eba5738 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 @@ -37,25 +37,11 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.R import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.models.UserInfo import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination - -/* -По умолчанию скрытое текстовое поле с ошибкой (main_error). - -Требования к компонентам: - -В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных. -Для получения данных необходимо использовать сетевой запрос /api//info. -При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации. -При нажатии кнопки бронирования необходимо открыть экран бронирования. -При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных. -Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января). - */ - - @Composable fun MainScreen( viewModel: MainViewModel = viewModel(), @@ -71,59 +57,72 @@ fun MainScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TestIds.Main.REFRESH_BUTTON), - onClick = { /* обновить данные */ } - ) { - Text(stringResource(R.string.refresh)) - } - Spacer(modifier = Modifier.size(8.dp)) - - when (state) { + when (val s = state) { is MainState.Error -> { Text( - text = (state as MainState.Error).message, + text = s.message, color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.ERROR) ) + Spacer(modifier = Modifier.size(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = viewModel::onRefresh + ) { + Text(stringResource(R.string.refresh)) + } } is MainState.Loading -> { CircularProgressIndicator() } is MainState.Data -> { - MainContent(navController = navController) + MainContent( + userInfo = s.userInfo, + navController = navController, + onRefresh = viewModel::onRefresh + ) } } } } @Composable -fun MainContent(navController: NavController) { - val bookings = listOf( - Booking(date = "2025-12-01", place = "Аудитория 1"), - Booking(date = "2025-12-01", place = "Аудитория 2"), - Booking(date = "2025-12-02", place = "Аудитория 3"), - Booking(date = "2025-12-02", place = "Конференц-зал"), - Booking(date = "2025-12-03", place = "Аудитория с очень длинным названием. Lorem ipsum"), - Booking(date = "2025-12-03", place = "Лаборатория №101"), - Booking(date = "2025-12-04", place = "Переговорная комната"), - Booking(date = "2025-12-04", place = "Спортивный зал"), - ) +fun MainContent( + userInfo: UserInfo, + navController: NavController, + onRefresh: () -> Unit +) { + val bookings = remember { + userInfo.booking.entries.sortedBy { it.key }.map { Booking(it.key, it.value.place) } + } Column( modifier = Modifier.fillMaxSize() ) { + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = onRefresh + ) { + Text(stringResource(R.string.refresh)) + } + + Spacer(modifier = Modifier.size(8.dp)) + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { AsyncImage( - model = "https://palyulin.ru/netcat_files/23/21/rabotnik.jpg", + model = userInfo.photoUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -132,7 +131,7 @@ fun MainContent(navController: NavController) { ) Spacer(modifier = Modifier.size(16.dp)) Text( - text = "Иванов Иван Иванович", + text = userInfo.name, style = MaterialTheme.typography.titleLarge ) } @@ -236,4 +235,4 @@ fun BookCard( 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/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt index 680d44a..b02d4c6 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -1,7 +1,9 @@ package ru.myitschool.work.ui.screen.main +import ru.myitschool.work.data.models.UserInfo + sealed interface MainState { - data object Data : MainState - data object Loading : MainState + object Loading : MainState data class Error(val message: String) : MainState -} \ No newline at end of file + data class Data(val userInfo: UserInfo) : MainState +} 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 80d525e..63c359c 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,42 +2,34 @@ package ru.myitschool.work.ui.screen.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import ru.myitschool.work.data.repo.AuthRepository -import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase +import ru.myitschool.work.domain.GetUserInfoUseCase class MainViewModel : ViewModel() { - private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(MainState.Data) - val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState = _uiState.asStateFlow() - fun onIntent(intent: MainIntent) { -// when (intent) { -// is MainIntent.Send -> { -// viewModelScope.launch(Dispatchers.Default) { -// _uiState.update { MainState.Loading } -// checkAndSaveAuthCodeUseCase.invoke("9999").fold( -// onSuccess = { -// _actionFlow.emit(Unit) -// }, -// onFailure = { error -> -// error.printStackTrace() -// _actionFlow.emit(Unit) -// } -// ) -// } -// } -// is MainIntent.TextInput -> Unit -// } + private val getUserInfoUseCase = GetUserInfoUseCase() + + init { + loadData() } -} \ No newline at end of file + + fun onRefresh() { + loadData() + } + + private fun loadData() { + viewModelScope.launch { + _uiState.value = MainState.Loading + getUserInfoUseCase().onSuccess { + _uiState.value = MainState.Data(it) + }.onFailure { + _uiState.value = MainState.Error(it.message ?: "Unknown error") + } + } + } +} -- 2.34.1