diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ccda1..9b849bb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,7 +36,7 @@ android { dependencies { defaultComposeLibrary() - implementation("androidx.datastore:datastore-preferences:1.1.7") + implementation("androidx.datastore:datastore-preferences:1.2.0") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0") implementation("androidx.navigation:navigation-compose:2.9.6") val coil = "3.3.0" 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..f1a13ec 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.121: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/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..1807f66 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 @@ -1,16 +1,42 @@ package ru.myitschool.work.data.repo +import android.content.Context +import android.util.Log +import ru.myitschool.work.App import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { private var codeCache: String? = null + private const val PREF_NAME = "auth_prefs" + private const val KEY_SAVED_CODE = "saved_code" + private val context: Context get() = App.context - suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text + private fun loadSavedCode(): String? { + return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(KEY_SAVED_CODE, null) + } + + fun getSavedCode(): String? = loadSavedCode() + + suspend fun checkAndSave(text: String): Result { + return NetworkDataSource.checkAuth(text).fold( + onSuccess = { success -> + if (success) { + codeCache = text + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_SAVED_CODE, text) + .apply() + Result.success(Unit) + } else { + Result.failure(IllegalStateException("Неверный код для авторизации")) + } + }, + onFailure = { error -> + Log.e("AuthRepository", "Auth failed", error) + Result.failure(error) } - } + ) } } \ No newline at end of file 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 new file mode 100644 index 0000000..f768094 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.ui.screen.UserInfo +import androidx.core.content.edit + +object MainRepository { + suspend fun loadUserInfo(code: String): Result { + return NetworkDataSource.getInfo(code) + } + fun clearAuth() { + val prefs = ru.myitschool.work.App.context + .getSharedPreferences("auth_prefs", android.content.Context.MODE_PRIVATE) + prefs.edit { clear() } + } +} \ No newline at end of file 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..89b7c79 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 @@ -1,6 +1,10 @@ package ru.myitschool.work.data.source +import android.icu.text.IDNA +import android.service.autofill.UserData +import android.util.Log import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get @@ -11,16 +15,19 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.ui.screen.UserInfo object NetworkDataSource { private val client by lazy { HttpClient(CIO) { + engine { requestTimeout= 10000; } + install(ContentNegotiation) { json( Json { isLenient = true ignoreUnknownKeys = true - explicitNulls = true + explicitNulls = false encodeDefaults = true } ) @@ -31,12 +38,29 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) + Log.d("NetworkDataSource", "Auth response: ${response.status}") when (response.status) { HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + else -> error("Неверный код для авторизации") + } + }.onFailure { error -> + Log.e("NetworkDataSource", "Auth request failed", error) + } + } + suspend fun getInfo(code: String): Result = withContext(Dispatchers.IO){ + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when(response.status){ + HttpStatusCode.OK -> response.body() + else -> error("Ошибка получения данных") } } } - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" -} \ No newline at end of file + private const val TAG = "NetworkDataSource" + + private fun getUrl(code: String, targetUrl: String): String { + val url = "${Constants.HOST}api/$code$targetUrl" + Log.d(TAG, "URL: $url") + return url + }} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 012fb6f..733d95c 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt @@ -5,11 +5,7 @@ import ru.myitschool.work.data.repo.AuthRepository class CheckAndSaveAuthCodeUseCase( private val repository: AuthRepository ) { - suspend operator fun invoke( - text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") - } + suspend operator fun invoke(text: String): Result { + return repository.checkAndSave(text) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt new file mode 100644 index 0000000..8834207 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt @@ -0,0 +1,23 @@ +package ru.myitschool.work.ui.screen + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val name: String, + @SerialName("photoUrl") val photoUrl: String?, + @SerialName("booking") val booking: Map +) { + val bookings: List by lazy { + booking.map { (date, item) -> + Booking(date = date, place = item.place) + }.sortedBy { it.date } + } +} + +@Serializable +data class BookingItem(val place: String, val id: Int) + +@Serializable +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/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 01b0f32..3409131 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,42 +7,45 @@ 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 import androidx.navigation.compose.rememberNavController +import ru.myitschool.work.data.repo.AuthRepository 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 @Composable fun AppNavHost( modifier: Modifier = Modifier, navController: NavHostController = rememberNavController() ) { + val startDestination = if (AuthRepository.getSavedCode() != null) { + MainScreenDestination + } else { + AuthScreenDestination + } NavHost( modifier = modifier, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, navController = navController, - startDestination = AuthScreenDestination, + startDestination = startDestination ) { composable { AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen( + viewModel = viewModel(), + navController = navController + ) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") + Box(contentAlignment = Alignment.Center) { + Text("Hello") } } } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/UserEntity.kt b/app/src/main/java/ru/myitschool/work/ui/screen/UserEntity.kt new file mode 100644 index 0000000..e69de29 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..e22a5e4 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column 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.material3.Button @@ -21,7 +22,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -74,6 +74,11 @@ private fun Content( state: AuthState.Data ) { var inputText by remember { mutableStateOf("") } + + val isValidCode = inputText.length == 4 && inputText.isNotEmpty() && inputText.none { it.isWhitespace() } && inputText.all { ch -> + ch in '0'..'9' || ch in 'A'..'Z' || ch in 'a'..'z' + } + Spacer(modifier = Modifier.size(16.dp)) TextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), @@ -82,15 +87,23 @@ private fun Content( inputText = it viewModel.onIntent(AuthIntent.TextInput(it)) }, - label = { Text(stringResource(R.string.auth_label)) } + label = { Text(stringResource(R.string.auth_label)) }, ) + if (state.error != null) { + Text( + text = state.error, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .testTag(TestIds.Auth.ERROR) + ) + } Spacer(modifier = Modifier.size(16.dp)) Button( modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), onClick = { viewModel.onIntent(AuthIntent.Send(inputText)) }, - enabled = true + enabled = isValidCode ) { 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..9c1a8dc 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 @@ -2,5 +2,7 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthState { object Loading: AuthState - object Data: AuthState + data class Data( + val error: String? = null + ) : 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..e6d3779 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 @@ -15,7 +15,7 @@ import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase class AuthViewModel : ViewModel() { private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } - private val _uiState = MutableStateFlow(AuthState.Data) + private val _uiState = MutableStateFlow(AuthState.Data()) val uiState: StateFlow = _uiState.asStateFlow() private val _actionFlow: MutableSharedFlow = MutableSharedFlow() @@ -24,20 +24,23 @@ class AuthViewModel : ViewModel() { fun onIntent(intent: AuthIntent) { when (intent) { is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( + checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { _actionFlow.emit(Unit) }, onFailure = { error -> - error.printStackTrace() - _actionFlow.emit(Unit) + _uiState.update { + AuthState.Data(error.message ?: "Неверный код для авторизации") + } } ) } } - is AuthIntent.TextInput -> Unit + is AuthIntent.TextInput -> { + _uiState.update { AuthState.Data() } + } } } } \ 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..d7b99f3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainIntent { + data object LoadData : MainIntent + data object Logout : MainIntent + data object AddBooking : 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..507ac27 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,223 @@ +package ru.myitschool.work.ui.screen.main + +import android.util.Log +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +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.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.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.screen.Booking + +@Composable +fun MainScreen( + viewModel: MainViewModel = viewModel(), + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + LaunchedEffect(state) { + Log.d("MainScreen", "UI State: $state") + } + LaunchedEffect(viewModel) { + viewModel.navigationFlow.collect { event -> + when (event) { + MainNavigationEvent.NavigateToAuth -> { + navController.navigate(AuthScreenDestination) { + popUpTo(navController.graph.startDestinationId) { inclusive = true } + } + } + MainNavigationEvent.NavigateToBook -> { + navController.navigate(BookScreenDestination) + } + } + } + } + + when (state) { + MainState.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) + } + is MainState.Content -> { + Content( + state = state as MainState.Content, + onRefresh = { viewModel.onIntent(MainIntent.LoadData) }, + onLogout = { viewModel.onIntent(MainIntent.Logout) }, + onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) } + ) + } + is MainState.ErrorOnly -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = (state as MainState.ErrorOnly).message, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { viewModel.onIntent(MainIntent.LoadData) }, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + } + } + } +} + +@Composable +private fun Content( + state: MainState.Content, + onRefresh: () -> Unit, + onLogout: () -> Unit, + onAddBooking: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + state.userPhotoUrl?.let { url -> + AsyncImage( + model = url, + contentDescription = "Аватар", + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) + ) + } + state.userName?.let { + Text( + text = it, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Button( + onClick = onLogout, + modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) + ) { + Text("Выйти") + } + Button( + onClick = onRefresh, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + Text("Обновить") + } + Button( + onClick = onAddBooking, + modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) + ) { + Text("Забронировать") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + if (state.error != null) { + Text( + text = state.error, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) + Spacer(modifier = Modifier.height(8.dp)) + } + Text( + text = "Бронирования", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .fillMaxWidth() + .testTag("main_bookings_title") + ) + + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Дата", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.testTag("main_bookings_header_date") + ) + Text( + text = "Место", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.testTag("main_bookings_header_place") + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + LazyColumn(modifier = Modifier.fillMaxSize()) { + itemsIndexed(state.bookings) { index, item -> + BookingItemView(booking = item, index = index) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} +@Composable +private fun BookingItemView(booking: Booking, index: Int) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag(TestIds.Main.getIdItemByPosition(index)) + .padding(8.dp) + ) { + Text( + text = booking.date, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = booking.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/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt new file mode 100644 index 0000000..29e8434 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.ui.screen.main + +import ru.myitschool.work.ui.screen.Booking + +sealed interface MainState { + object Loading : MainState + data class Content( + val userName: String?, + val userPhotoUrl: String?, + val bookings: List, + val error: String? = null + ) : MainState + data class ErrorOnly( + val message: String + ) : 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..8fd7dea --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,103 @@ +package ru.myitschool.work.ui.screen.main + +import android.util.Log +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 kotlinx.coroutines.withContext +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.ui.screen.Booking +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class MainViewModel : ViewModel() { + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _navigationFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) + val navigationFlow: SharedFlow = _navigationFlow + fun formatDateString(isoDate: String): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + val date = LocalDate.parse(isoDate, inputFormatter) + return date.format(outputFormatter) + } + init { + loadData() + } + + private fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + _uiState.value = MainState.Loading + } + + val code = AuthRepository.getSavedCode() ?: run { + _navigationFlow.emit(MainNavigationEvent.NavigateToAuth) + return@launch + } + + MainRepository.loadUserInfo(code).fold( + onSuccess = { userInfo -> + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + + val sortedBookings = userInfo.bookings + .sortedBy { LocalDate.parse(it.date, inputFormatter) } + .map { booking -> + Booking( + date = LocalDate.parse(booking.date, inputFormatter).format(outputFormatter), + place = booking.place + ) + } + + withContext(Dispatchers.Main) { + _uiState.value = MainState.Content( + userName = userInfo.name, + userPhotoUrl = userInfo.photoUrl ?: "", + bookings = sortedBookings, + error = null + ) + } + }, + onFailure = { error -> + Log.e("MainViewModel", "Ошибка загрузки", error) + withContext(Dispatchers.Main) { + _uiState.value = MainState.ErrorOnly( + error.message ?: "Не удалось загрузить данные" + ) + } + } + ) + } + } + + fun onIntent(intent: MainIntent) { + when (intent) { + MainIntent.LoadData -> loadData() + MainIntent.Logout -> { + MainRepository.clearAuth() + viewModelScope.launch { + _navigationFlow.emit(MainNavigationEvent.NavigateToAuth) + } + } + MainIntent.AddBooking -> { + viewModelScope.launch { + _navigationFlow.emit(MainNavigationEvent.NavigateToBook) + } + } + } + } +} +sealed interface MainNavigationEvent { + object NavigateToAuth : MainNavigationEvent + object NavigateToBook : MainNavigationEvent +} \ No newline at end of file