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 74c08fe..b95bd0e 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 @@ -19,6 +19,7 @@ object MainRepository { @Serializable data class UserDto( val name: String, + val photoUrl: String, val bookingList: List ) @@ -43,6 +44,7 @@ object MainRepository { val user = UserEntity( name = userResponse.name, + photoUrl = userResponse.photoUrl, bookingList = userResponse.bookingList.map { BookingEntity(it.roomName, it.time) } ) diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetUserInfoUseCase.kt index 6f51002..43aec43 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/GetUserInfoUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetUserInfoUseCase.kt @@ -4,9 +4,9 @@ import ru.myitschool.work.data.repo.MainRepository import ru.myitschool.work.domain.auth.entities.UserEntity class GetUserInfoUseCase( - private val repository: MainRepository + private val repository: MainRepository = MainRepository ) { suspend operator fun invoke(code: String): Result { return repository.getUserInfo(code) } -} +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/auth/entities/UserEntity.kt index 891d7c8..d5050b3 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/entities/UserEntity.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/entities/UserEntity.kt @@ -2,5 +2,6 @@ package ru.myitschool.work.domain.auth.entities data class UserEntity( val name: String, + val photoUrl: String, val bookingList: List ) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/entities/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/main/entities/GetUserInfoUseCase.kt new file mode 100644 index 0000000..f033725 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/entities/GetUserInfoUseCase.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.domain.main + +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.domain.auth.entities.UserEntity + +class GetUserInfoUseCase( + private val repository: MainRepository = MainRepository +) { + suspend operator fun invoke(code: String): Result { + return repository.getUserInfo(code) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt index deca45f..9f77f24 100644 --- a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt +++ b/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt @@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav import kotlinx.serialization.Serializable @Serializable -data object MainScreenDestination: AppDestination \ No newline at end of file +data class MainScreenDestination(val authCode: 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 01b0f32..740be38 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,18 +3,26 @@ package ru.myitschool.work.ui.screen import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable +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.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import androidx.navigation.toRoute 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.main.MainScreen +import ru.myitschool.work.ui.screen.main.MainViewModel @Composable fun AppNavHost( @@ -26,23 +34,42 @@ fun AppNavHost( enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, + startDestination = AuthScreenDestination ) { composable { AuthScreen(navController = navController) } - composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + + composable { backStackEntry -> + val authCode = backStackEntry.toRoute().authCode + + val viewModel = remember(authCode) { MainViewModel(authCode) } + val state by viewModel.uiState.collectAsState() + + MainScreen( + state = state, + onRefresh = { viewModel.onRefresh() }, + onLogout = { + viewModel.onLogout() + navController.navigate(AuthScreenDestination) { + popUpTo(navController.graph.startDestinationId) { + inclusive = true + } + launchSingleTop = true + } + }, + onBookClick = { + navController.navigate(BookScreenDestination) + } + ) } + composable { Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Text(text = "Hello") + Text(text = "Экран бронирования (в разработке)") } } } 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 db2982e..ddc87bf 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,7 +31,6 @@ import ru.myitschool.work.R import ru.myitschool.work.core.TestIds import ru.myitschool.work.ui.nav.MainScreenDestination - @Composable fun AuthScreen( viewModel: AuthViewModel = viewModel(), @@ -39,9 +38,9 @@ fun AuthScreen( ) { val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) + LaunchedEffect(viewModel) { + viewModel.actionFlow.collect { authCode -> + navController.navigate(MainScreenDestination(authCode = authCode)) } } @@ -67,7 +66,6 @@ fun AuthScreen( } } } - @Composable private fun Content( viewModel: AuthViewModel, @@ -76,9 +74,9 @@ private fun Content( var inputText by remember { mutableStateOf("") } val isCodeValid = inputText.length == 4 && inputText.all { it.isLetterOrDigit() } - val isButtonEnabled = isCodeValid Spacer(modifier = Modifier.size(16.dp)) + TextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), value = inputText, @@ -107,9 +105,8 @@ private fun Content( onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, - enabled = isButtonEnabled + enabled = isCodeValid ) { Text(stringResource(R.string.auth_sign_in)) } -} - +} \ 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 b00a078..f16ec43 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 @@ -8,7 +8,6 @@ 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 @@ -18,8 +17,8 @@ class AuthViewModel : ViewModel() { private val _uiState = MutableStateFlow(AuthState.Data()) val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: AuthIntent) { when (intent) { @@ -29,7 +28,7 @@ class AuthViewModel : ViewModel() { _uiState.value = AuthState.Loading checkAndSaveAuthCodeUseCase.invoke(code).fold( onSuccess = { - _actionFlow.emit(Unit) + _actionFlow.emit(intent.text) }, onFailure = { error -> 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..2c3becd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,150 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import ru.myitschool.work.core.TestIds + +@Composable +fun MainScreen( + state: MainState, + onRefresh: () -> Unit, + onLogout: () -> Unit, + onBookClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + when (state) { + is MainState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + + is MainState.Error -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = onRefresh, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + } + } + + is MainState.Success -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + AsyncImage( + model = state.photoUrl, + contentDescription = "Аватар", + modifier = Modifier + .size(56.dp) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = state.name, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.REFRESH_BUTTON), + onClick = onRefresh + ) { + Text("Обновить") + } + Button( + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.LOGOUT_BUTTON), + onClick = onLogout + ) { + Text("Выйти") + } + Button( + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.ADD_BUTTON), + onClick = onBookClick + ) { + Text("Забронировать") + } + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(state.bookings) { index, item -> + BookingRow(item = item, index = index) + } + } + } + } + } + } +} + +@Composable +fun AsyncImage(model: String, contentDescription: String, modifier: Modifier) { +} + +@Composable +private fun BookingRow(item: BookingItem, index: Int) { + Card( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.getIdItemByPosition(index)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = item.date, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) + ) + Text( + text = item.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/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt new file mode 100644 index 0000000..c866cca --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,75 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.myitschool.work.domain.main.GetUserInfoUseCase + +class MainViewModel( + private val authCode: String +) : ViewModel() { + + private val getUserInfoUseCase = GetUserInfoUseCase() + + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadUserInfo() + } + + fun onRefresh() { + loadUserInfo() + } + + fun onLogout() { + } + + fun onBookClick() { + } + + + private fun loadUserInfo() { + viewModelScope.launch { + getUserInfoUseCase.invoke(authCode).fold( + onSuccess = { user -> + val bookings = user.bookingList.map { bookingEntity -> + BookingItem( + date = formatDate(bookingEntity.time), + place = bookingEntity.roomName + ) + }.sortedBy { it.date } + + _uiState.value = MainState.Success( + name = user.name, + photoUrl = user.photoUrl, + bookings = bookings + ) + }, + onFailure = { error -> + val msg = when { + error.message?.contains("401") == true -> "Пользователь не найден" + else -> "Не удалось загрузить данные" + } + _uiState.value = MainState.Error(msg) + } + ) + } + } + + private fun formatDate(input: String): String { + return try { + val parts = input.split("-") + if (parts.size == 3) { + "${parts[2]}.${parts[1]}.${parts[0]}" + } else { + input + } + } catch (e: Exception) { + input + } + } +} \ No newline at end of file