From 3f514588638c853fbfcb53ffcde20067172eb82a Mon Sep 17 00:00:00 2001 From: imglmd Date: Mon, 1 Dec 2025 22:45:08 +0300 Subject: [PATCH] mainScreen --- .../work/data/repo/UserRepository.kt | 8 +- .../ru/myitschool/work/domain/user/User.kt | 5 +- .../ru/myitschool/work/ui/main/BookingItem.kt | 48 +++++ .../ru/myitschool/work/ui/main/MainIntent.kt | 1 + .../ru/myitschool/work/ui/main/MainScreen.kt | 191 +++++++++++++++++- .../ru/myitschool/work/ui/main/MainState.kt | 10 +- .../myitschool/work/ui/main/MainViewModel.kt | 38 +++- 7 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/main/BookingItem.kt diff --git a/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt index 9bd7da0..ce6caa3 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt @@ -8,14 +8,16 @@ import ru.myitschool.work.domain.user.User object UserRepository { suspend fun getUserInfo(code: String): MyResult { - return when (val result = NetworkDataSource.getUserInfo(code)) { + /*return when (val result = NetworkDataSource.getUserInfo(code)) { is MyResult.Success -> MyResult.Success(result.data.toUser()) is MyResult.Error -> result - } + }*/ + return MyResult.Success(User("kio", "https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg")) } private fun UserInfoResponse.toUser() = User( name = name, - imageUrl = photoUrl + imageUrl = photoUrl, + bookings = booking.mapValues { it.value.place } ) } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/user/User.kt b/app/src/main/java/ru/myitschool/work/domain/user/User.kt index 9155712..8c524a6 100644 --- a/app/src/main/java/ru/myitschool/work/domain/user/User.kt +++ b/app/src/main/java/ru/myitschool/work/domain/user/User.kt @@ -2,5 +2,6 @@ package ru.myitschool.work.domain.user data class User( val name: String, - val imageUrl: String -) + val imageUrl: String, + val bookings: Map = emptyMap() +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/main/BookingItem.kt b/app/src/main/java/ru/myitschool/work/ui/main/BookingItem.kt new file mode 100644 index 0000000..c4ebdfa --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/main/BookingItem.kt @@ -0,0 +1,48 @@ +package ru.myitschool.work.ui.main + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import ru.myitschool.work.core.TestIds + +@Composable +fun BookingItem( + booking: BookingDisplayItem, + index: Int +) { + Card( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.getIdItemByPosition(index)), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = booking.date, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE), + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = booking.place, + modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainIntent.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainIntent.kt index 20a058b..b0dc0af 100644 --- a/app/src/main/java/ru/myitschool/work/ui/main/MainIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainIntent.kt @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.main sealed interface MainIntent { data object LogOut: MainIntent data object Refresh: MainIntent + data object AddBooking: MainIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainScreen.kt index 2112ad7..0b98e05 100644 --- a/app/src/main/java/ru/myitschool/work/ui/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainScreen.kt @@ -1,21 +1,38 @@ package ru.myitschool.work.ui.main +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.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.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.dto.BookingItem import ru.myitschool.work.ui.Screen @Composable @@ -29,31 +46,183 @@ fun MainScreen( when (event) { MainViewModel.NavigationEvent.ToAuth -> { navController.navigate(Screen.Auth) { - popUpTo(navController.graph.startDestinationId) { inclusive = true } + popUpTo(Screen.Auth) { inclusive = true } launchSingleTop = true } } + + MainViewModel.NavigationEvent.ToBooking -> { + navController.navigate(Screen.Book) + } } } } - Scaffold() { padding -> - Column(Modifier.padding(padding)){ - AsyncImage(model = state.user.imageUrl, contentDescription = null, modifier = Modifier.testTag( - TestIds.Main.PROFILE_IMAGE)) - Text(text = state.user.name) + Scaffold { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + when { + state.error != null -> { + ErrorContent( + error = state.error!!, + onRefresh = { viewModel.onIntent(MainIntent.Refresh) } + ) + } + state.isLoading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + state.user != null -> { + MainContent( + state = state, + onLogout = { viewModel.onIntent(MainIntent.LogOut) }, + onRefresh = { viewModel.onIntent(MainIntent.Refresh) }, + onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) } + ) + } + } + } + } +} + +@Composable +private fun ErrorContent( + error: String, + onRefresh: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = error, + modifier = Modifier.testTag(TestIds.Main.ERROR), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = onRefresh, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + } +} + +@Composable +private fun MainContent( + state: MainState, + onLogout: () -> Unit, + onRefresh: () -> Unit, + onAddBooking: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + AsyncImage( + model = state.user?.imageUrl, + contentDescription = null, + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = state.user?.name ?: "", + modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME), + style = MaterialTheme.typography.headlineSmall + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { Button( - modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON), - onClick = { viewModel.onIntent(MainIntent.Refresh)} + onClick = onRefresh, + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.REFRESH_BUTTON) ) { Text("Обновить") } Button( - modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON), - onClick = { viewModel.onIntent(MainIntent.LogOut) } + onClick = onLogout, + modifier = Modifier + .weight(1f) + .testTag(TestIds.Main.LOGOUT_BUTTON) ) { Text("Выйти") } } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onAddBooking, + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.ADD_BUTTON) + ) { + Text("Забронировать") + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Мои бронирования", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (state.bookings.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "У вас пока нет бронирований", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(state.bookings) { index, booking -> + BookingItem( + booking = booking, + index = index + ) + } + } + } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainState.kt index a006f55..2a89d45 100644 --- a/app/src/main/java/ru/myitschool/work/ui/main/MainState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainState.kt @@ -3,7 +3,15 @@ package ru.myitschool.work.ui.main import ru.myitschool.work.domain.user.User data class MainState( - val user: User = User("", ""), + val user: User? = null, + val bookings: List = emptyList(), val isLoading: Boolean = false, val error: String? = null ) + +data class BookingDisplayItem( + val date: String, // dd.MM.yyyy + val place: String, + val originalDate: String +) + diff --git a/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt index 11216a0..ca12b63 100644 --- a/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/main/MainViewModel.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.main +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -14,8 +15,9 @@ import ru.myitschool.work.data.repo.UserRepository import ru.myitschool.work.domain.MyResult import ru.myitschool.work.domain.auth.LogOutUseCase import ru.myitschool.work.domain.user.GetUserUseCase -import ru.myitschool.work.domain.user.User import ru.myitschool.work.util.DataStoreManager +import java.text.SimpleDateFormat +import java.util.Locale class MainViewModel : ViewModel() { private val logOutUseCase = LogOutUseCase(AuthRepository) @@ -33,12 +35,14 @@ class MainViewModel : ViewModel() { sealed class NavigationEvent { data object ToAuth : NavigationEvent() + data object ToBooking : NavigationEvent() } fun onIntent(intent: MainIntent) { when (intent) { MainIntent.LogOut -> logOut() MainIntent.Refresh -> loadUser() + MainIntent.AddBooking -> navigateToBooking() } } @@ -54,9 +58,11 @@ class MainViewModel : ViewModel() { when (val result = getUserUseCase(code)) { is MyResult.Success -> { + val bookings = convertBookingsToDisplayItems(result.data.bookings) _mainState.update { it.copy( user = result.data, + bookings = bookings, isLoading = false, error = null ) @@ -67,7 +73,9 @@ class MainViewModel : ViewModel() { _mainState.update { it.copy( isLoading = false, - error = result.error + error = result.error, + user = null, + bookings = emptyList() ) } } @@ -75,10 +83,30 @@ class MainViewModel : ViewModel() { } } + + private fun convertBookingsToDisplayItems(bookings: Map): List { + val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + + return bookings.map { (date, place) -> + BookingDisplayItem( + date = date, + place = place, + originalDate = date + ) + }.sortedBy { item -> + try { + dateFormat.parse(item.originalDate)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } + } + private fun logOut() { viewModelScope.launch { when (logOutUseCase()) { is MyResult.Success -> { + _mainState.update { MainState() } _navigationEvent.emit(NavigationEvent.ToAuth) } is MyResult.Error -> { @@ -87,4 +115,10 @@ class MainViewModel : ViewModel() { } } } + + private fun navigateToBooking() { + viewModelScope.launch { + _navigationEvent.emit(NavigationEvent.ToBooking) + } + } } \ No newline at end of file