Merge pull request 'feature/auth-implementation' (#1) from feature/auth-implementation into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-12-06 11:57:49 +00:00
16 changed files with 494 additions and 39 deletions

View File

@@ -36,7 +36,7 @@ android {
dependencies { dependencies {
defaultComposeLibrary() 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("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
implementation("androidx.navigation:navigation-compose:2.9.6") implementation("androidx.navigation:navigation-compose:2.9.6")
val coil = "3.3.0" val coil = "3.3.0"

View File

@@ -1,7 +1,7 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { 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 AUTH_URL = "/auth"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" const val BOOKING_URL = "/booking"

View File

@@ -1,16 +1,42 @@
package ru.myitschool.work.data.repo 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 import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository { object AuthRepository {
private var codeCache: String? = null 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<Boolean> { private fun loadSavedCode(): String? {
return NetworkDataSource.checkAuth(text).onSuccess { success -> return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getString(KEY_SAVED_CODE, null)
}
fun getSavedCode(): String? = loadSavedCode()
suspend fun checkAndSave(text: String): Result<Unit> {
return NetworkDataSource.checkAuth(text).fold(
onSuccess = { success ->
if (success) { if (success) {
codeCache = text 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)
} }
)
} }
} }

View File

@@ -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<UserInfo> {
return NetworkDataSource.getInfo(code)
}
fun clearAuth() {
val prefs = ru.myitschool.work.App.context
.getSharedPreferences("auth_prefs", android.content.Context.MODE_PRIVATE)
prefs.edit { clear() }
}
}

View File

@@ -1,6 +1,10 @@
package ru.myitschool.work.data.source 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.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get import io.ktor.client.request.get
@@ -11,16 +15,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.ui.screen.UserInfo
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val client by lazy {
HttpClient(CIO) { HttpClient(CIO) {
engine { requestTimeout= 10000; }
install(ContentNegotiation) { install(ContentNegotiation) {
json( json(
Json { Json {
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
explicitNulls = true explicitNulls = false
encodeDefaults = true encodeDefaults = true
} }
) )
@@ -31,12 +38,29 @@ object NetworkDataSource {
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.get(getUrl(code, Constants.AUTH_URL))
Log.d("NetworkDataSource", "Auth response: ${response.status}")
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> error(response.bodyAsText()) else -> error("Неверный код для авторизации")
}
}.onFailure { error ->
Log.e("NetworkDataSource", "Auth request failed", error)
}
}
suspend fun getInfo(code: String): Result<UserInfo> = 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" 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
}}

View File

@@ -5,11 +5,7 @@ import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase( class CheckAndSaveAuthCodeUseCase(
private val repository: AuthRepository private val repository: AuthRepository
) { ) {
suspend operator fun invoke( suspend operator fun invoke(text: String): Result<Unit> {
text: String return repository.checkAndSave(text)
): Result<Unit> {
return repository.checkAndSave(text).mapCatching { success ->
if (!success) error("Code is incorrect")
}
} }
} }

View File

@@ -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<String, BookingItem>
) {
val bookings: List<Booking> 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)

View File

@@ -7,42 +7,45 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
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 ru.myitschool.work.data.repo.AuthRepository
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
@Composable @Composable
fun AppNavHost( fun AppNavHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController() navController: NavHostController = rememberNavController()
) { ) {
val startDestination = if (AuthRepository.getSavedCode() != null) {
MainScreenDestination
} else {
AuthScreenDestination
}
NavHost( NavHost(
modifier = modifier, modifier = modifier,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
navController = navController, navController = navController,
startDestination = AuthScreenDestination, startDestination = startDestination
) { ) {
composable<AuthScreenDestination> { composable<AuthScreenDestination> {
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> { composable<MainScreenDestination> {
Box( MainScreen(
contentAlignment = Alignment.Center viewModel = viewModel(),
) { navController = navController
Text(text = "Hello") )
}
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
Box( Box(contentAlignment = Alignment.Center) {
contentAlignment = Alignment.Center Text("Hello")
) {
Text(text = "Hello")
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -21,7 +22,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -74,6 +74,11 @@ private fun Content(
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") } 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)) Spacer(modifier = Modifier.size(16.dp))
TextField( TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
@@ -82,15 +87,23 @@ private fun Content(
inputText = it inputText = it
viewModel.onIntent(AuthIntent.TextInput(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)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) viewModel.onIntent(AuthIntent.Send(inputText))
}, },
enabled = true enabled = isValidCode
) { ) {
Text(stringResource(R.string.auth_sign_in)) Text(stringResource(R.string.auth_sign_in))
} }

View File

@@ -2,5 +2,7 @@ package ru.myitschool.work.ui.screen.auth
sealed interface AuthState { sealed interface AuthState {
object Loading: AuthState object Loading: AuthState
object Data: AuthState data class Data(
val error: String? = null
) : AuthState
} }

View File

@@ -15,7 +15,7 @@ import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class AuthViewModel : ViewModel() { class AuthViewModel : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) } private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
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<Unit> = MutableSharedFlow()
@@ -24,20 +24,23 @@ class AuthViewModel : ViewModel() {
fun onIntent(intent: AuthIntent) { fun onIntent(intent: AuthIntent) {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.IO) {
_uiState.update { AuthState.Loading } _uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold( checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(Unit) _actionFlow.emit(Unit)
}, },
onFailure = { error -> onFailure = { error ->
error.printStackTrace() _uiState.update {
_actionFlow.emit(Unit) AuthState.Data(error.message ?: "Неверный код для авторизации")
}
} }
) )
} }
} }
is AuthIntent.TextInput -> Unit is AuthIntent.TextInput -> {
_uiState.update { AuthState.Data() }
}
} }
} }
} }

View File

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

View File

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

View File

@@ -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<Booking>,
val error: String? = null
) : MainState
data class ErrorOnly(
val message: String
) : MainState
}

View File

@@ -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>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _navigationFlow = MutableSharedFlow<MainNavigationEvent>(replay = 0, extraBufferCapacity = 1)
val navigationFlow: SharedFlow<MainNavigationEvent> = _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
}