main #2

Merged
student-15031 merged 7 commits from student-28725/NTO-2025-Android-TeamTask:main into main 2025-11-29 16:46:27 +00:00
9 changed files with 285 additions and 23 deletions
Showing only changes of commit ca67f90884 - Show all commits

View File

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

View File

@@ -4,7 +4,7 @@ import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.auth.entities.UserEntity import ru.myitschool.work.domain.auth.entities.UserEntity
class GetUserInfoUseCase( class GetUserInfoUseCase(
private val repository: MainRepository private val repository: MainRepository = MainRepository
) { ) {
suspend operator fun invoke(code: String): Result<UserEntity> { suspend operator fun invoke(code: String): Result<UserEntity> {
return repository.getUserInfo(code) return repository.getUserInfo(code)

View File

@@ -2,5 +2,6 @@ package ru.myitschool.work.domain.auth.entities
data class UserEntity( data class UserEntity(
val name: String, val name: String,
val photoUrl: String,
val bookingList: List<BookingEntity> 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,18 +3,26 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController 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.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen 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 @Composable
fun AppNavHost( fun AppNavHost(
@@ -26,23 +34,42 @@ fun AppNavHost(
enterTransition = { EnterTransition.None }, enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None }, exitTransition = { ExitTransition.None },
navController = navController, navController = navController,
startDestination = AuthScreenDestination, startDestination = AuthScreenDestination
) { ) {
composable<AuthScreenDestination> { composable<AuthScreenDestination> {
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> {
Box( composable<MainScreenDestination> { backStackEntry ->
contentAlignment = Alignment.Center val authCode = backStackEntry.toRoute<MainScreenDestination>().authCode
) {
Text(text = "Hello") 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> { composable<BookScreenDestination> {
Box( Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center 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.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable @Composable
fun AuthScreen( fun AuthScreen(
viewModel: AuthViewModel = viewModel(), viewModel: AuthViewModel = viewModel(),
@@ -39,9 +38,9 @@ fun AuthScreen(
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(viewModel) {
viewModel.actionFlow.collect { viewModel.actionFlow.collect { authCode ->
navController.navigate(MainScreenDestination) navController.navigate(MainScreenDestination(authCode = authCode))
} }
} }
@@ -67,7 +66,6 @@ fun AuthScreen(
} }
} }
} }
@Composable @Composable
private fun Content( private fun Content(
viewModel: AuthViewModel, viewModel: AuthViewModel,
@@ -76,7 +74,6 @@ private fun Content(
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
val isCodeValid = inputText.length == 4 && inputText.all { it.isLetterOrDigit() } val isCodeValid = inputText.length == 4 && inputText.all { it.isLetterOrDigit() }
val isButtonEnabled = isCodeValid
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
@@ -108,9 +105,8 @@ private fun Content(
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(inputText))
}, },
enabled = isButtonEnabled enabled = isCodeValid
) { ) {
Text(stringResource(R.string.auth_sign_in)) 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.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
@@ -18,8 +17,8 @@ class AuthViewModel : ViewModel() {
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data()) private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
val uiState: StateFlow<AuthState> = _uiState.asStateFlow() val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow() private val _actionFlow = MutableSharedFlow<String>()
val actionFlow: SharedFlow<Unit> = _actionFlow val actionFlow: SharedFlow<String> = _actionFlow
fun onIntent(intent: AuthIntent) { fun onIntent(intent: AuthIntent) {
when (intent) { when (intent) {
@@ -29,7 +28,7 @@ class AuthViewModel : ViewModel() {
_uiState.value = AuthState.Loading _uiState.value = AuthState.Loading
checkAndSaveAuthCodeUseCase.invoke(code).fold( checkAndSaveAuthCodeUseCase.invoke(code).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(Unit) _actionFlow.emit(intent.text)
}, },
onFailure = { error -> 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
}
}
}