Merge pull request 'main' (#2) from student-28725/NTO-2025-Android-TeamTask:main into main
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled

Reviewed-on: student-15031/NTO-2025-Android-TeamTask#2
This commit is contained in:
2025-11-29 16:46:26 +00:00
10 changed files with 287 additions and 24 deletions

View File

@@ -19,6 +19,7 @@ object MainRepository {
@Serializable
data class UserDto(
val name: String,
val photoUrl: String,
val bookingList: List<BookingDto>
)
@@ -43,6 +44,7 @@ object MainRepository {
val user = UserEntity(
name = userResponse.name,
photoUrl = userResponse.photoUrl,
bookingList = userResponse.bookingList.map { BookingEntity(it.roomName, it.time) }
)

View File

@@ -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<UserEntity> {
return repository.getUserInfo(code)
}
}
}

View File

@@ -2,5 +2,6 @@ package ru.myitschool.work.domain.auth.entities
data class UserEntity(
val name: String,
val photoUrl: String,
val bookingList: List<BookingEntity>
)

View File

@@ -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<UserEntity> {
return repository.getUserInfo(code)
}
}

View File

@@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object MainScreenDestination: AppDestination
data class MainScreenDestination(val authCode: String)

View File

@@ -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<AuthScreenDestination> {
AuthScreen(navController = navController)
}
composable<MainScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
composable<MainScreenDestination> { backStackEntry ->
val authCode = backStackEntry.toRoute<MainScreenDestination>().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<BookScreenDestination> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
Text(text = "Экран бронирования (в разработке)")
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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>(AuthState.Data())
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
private val _actionFlow = MutableSharedFlow<String>()
val actionFlow: SharedFlow<String> = _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 ->

View File

@@ -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)
)
}
}
}

View File

@@ -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>(MainState.Loading)
val uiState: StateFlow<MainState> = _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
}
}
}