From 8a5fee532bd52728a1db7f159f435702c3646931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 8 Dec 2025 21:22:55 +0300 Subject: [PATCH] I completed all the tasks assigned to me and combined my part of the project with Egor's part. --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/repo/AuthRepository.kt | 32 ++- .../work/data/repo/BookRepository.kt | 9 +- .../work/data/repo/MainRepository.kt | 16 ++ .../work/data/source/NetworkDataSource.kt | 58 +++-- .../auth/CheckAndSaveAuthCodeUseCase.kt | 8 +- .../work/ui/screen/BookingUserInfo.kt | 28 ++- .../work/ui/screen/NavigationGraph.kt | 14 +- .../work/ui/screen/auth/AuthScreen.kt | 2 +- .../work/ui/screen/auth/AuthViewModel.kt | 4 +- .../work/ui/screen/book/BookIntent.kt | 4 +- .../work/ui/screen/book/BookScreen.kt | 154 ++++++------ .../work/ui/screen/book/BookState.kt | 5 +- .../work/ui/screen/book/BookViewModel.kt | 31 +-- .../work/ui/screen/main/MainIntent.kt | 7 + .../work/ui/screen/main/MainScreen.kt | 223 ++++++++++++++++++ .../work/ui/screen/main/MainState.kt | 16 ++ .../work/ui/screen/main/MainViewModel.kt | 102 ++++++++ 18 files changed, 570 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt 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 1aae4fd..690c2b0 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://127.0.0.1:8080" + const val HOST = "http://192.168.1.74: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 da5682d..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,6 +1,7 @@ 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 @@ -10,17 +11,32 @@ object AuthRepository { private const val PREF_NAME = "auth_prefs" private const val KEY_SAVED_CODE = "saved_code" private val context: Context get() = App.context + private fun loadSavedCode(): String? { -// return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) -// .getString(KEY_SAVED_CODE, null) - return "" + 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).onSuccess { success -> - if (success) { - codeCache = text + + 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/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt index 2887f8e..d36e059 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -1,15 +1,14 @@ package ru.myitschool.work.data.repo import ru.myitschool.work.data.source.NetworkDataSource -import ru.myitschool.work.ui.screen.Booking -import ru.myitschool.work.ui.screen.UserInfo +import ru.myitschool.work.ui.screen.BookingItem object BookRepository { - suspend fun loadBooks(code: String): Result> { + suspend fun loadBooks(code: String): Result>> { return NetworkDataSource.getFreeBooking(code) } - suspend fun sendData(code: String, booking: Booking) : Result { - return NetworkDataSource.createNewBooking(code, booking) + suspend fun sendData(code: String, data: String, placeId: Int) : Result { + return NetworkDataSource.createNewBooking(code, data, placeId) } } \ 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 62c7504..aee6910 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,11 +1,13 @@ package ru.myitschool.work.data.source +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 import io.ktor.client.request.post +import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode @@ -14,9 +16,12 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.InternalAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.ui.screen.BookingItem +import ru.myitschool.work.ui.screen.CreateBookingRequest import ru.myitschool.work.ui.screen.UserInfo object NetworkDataSource { @@ -35,49 +40,64 @@ 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("Неверный код для авторизации") } + }.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() + HttpStatusCode.OK -> { + response.body() + } else -> error("Ошибка получения данных") } } } - suspend fun getFreeBooking(code: String) : Result> = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.get(getUrl(code, Constants.BOOKING_URL)) - when (response.status) { - HttpStatusCode.OK -> response.body() - else -> error(response.bodyAsText()) + + + + suspend fun getFreeBooking(code: String): Result>> = + withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> { + // Используйте response.body с явным указанием типа + response.body>>() + } + else -> error(response.bodyAsText()) + } } } - } + @OptIn(InternalAPI::class) - suspend fun createNewBooking(code: String, booking: Booking) : Result = withContext(Dispatchers.IO) { - return@withContext runCatching { - val response = client.post(getUrl(code, Constants.BOOK_URL)) { - contentType(ContentType.Application.Json) - body = booking - } - when (response.status) { - HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + suspend fun createNewBooking(code: String, data: String, placeId: Int): Result = + withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.post(getUrl(code, Constants.BOOK_URL)) { + contentType(ContentType.Application.Json) + setBody(CreateBookingRequest(data, placeId)) // используйте setBody вместо body + } + + when (response.status) { + HttpStatusCode.OK -> true + else -> error(response.bodyAsText()) + } } } - } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ 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 index 39c3e4b..0874863 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/BookingUserInfo.kt @@ -5,12 +5,34 @@ import kotlinx.serialization.Serializable @Serializable data class UserInfo( val name: String, - val photo: String? = null, - val bookings: List -) + val photoUrl: String? = null, + val booking: Map> +) { + fun bookingToList() : List { + val bookings = mutableListOf() + booking.forEach { + it.value.forEach { value -> + bookings.add(Booking(it.key, value.place)) + } + } + return bookings + } +} @Serializable data class Booking( val date: String, val place: String +) + +@Serializable +data class BookingItem( + val id: Int, + val place: String +) + +@Serializable +data class CreateBookingRequest( + val date: String, + val placeId: Int ) \ 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 b70bbc0..479c17c 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen +import android.util.Log import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Box @@ -18,6 +19,8 @@ import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.book.BookViewModel +import ru.myitschool.work.ui.screen.main.MainScreen +import ru.myitschool.work.ui.screen.main.MainViewModel @Composable fun AppNavHost( @@ -32,18 +35,17 @@ fun AppNavHost( startDestination = AuthScreenDestination, ) { composable { + Log.d("compose", "Auth") AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + Log.d("compose", "Main") + MainScreen(viewModel = viewModel(), navController) } composable { + Log.d("compose", "Book") BookScreen( - viewModel = BookViewModel(), + viewModel = viewModel(), navController = navController ) } 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 ccbb6df..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 @@ -75,7 +75,7 @@ private fun Content( ) { var inputText by remember { mutableStateOf("") } - val isValidCode = inputText.length >= 4 && inputText.isNotEmpty() && inputText.none { it.isWhitespace() } && inputText.all { ch -> + 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' } 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 fe7d469..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 @@ -24,11 +24,11 @@ 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(intent.text).fold( onSuccess = { - _actionFlow.emit(Unit)// переход на MainScreen + _actionFlow.emit(Unit) }, onFailure = { error -> _uiState.update { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt index 8d77d08..d983c38 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -1,9 +1,7 @@ package ru.myitschool.work.ui.screen.book -import ru.myitschool.work.ui.screen.Booking - sealed interface BookIntent { - data class Send(val code : String, val booking: Booking) : BookIntent + data class Send(val code : String, val data: String, val placeId : Int) : BookIntent data object BackToMainScreen : BookIntent data object ToAuthScreen : BookIntent data object LoadData : BookIntent diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 5772cd6..b76cd92 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -18,10 +18,11 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -34,11 +35,10 @@ import ru.myitschool.work.core.TestIds import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination -import ru.myitschool.work.ui.screen.Booking +import java.time.LocalDate +import java.time.format.DateTimeFormatter + -var selectedTime: MutableState = mutableStateOf(null) -var selectedBooking: MutableState = mutableStateOf(null) -var currentTime: MutableState = mutableStateOf(null) @Composable fun BookScreen( viewModel: BookViewModel = viewModel(), @@ -76,22 +76,93 @@ fun BookScreen( // Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) when (state) { is BookState.Data -> { + var selectedTime : String? by remember { mutableStateOf(null) } + var selectedBooking : Int? by remember { mutableStateOf(null) } + var currentTime : String? by remember { mutableStateOf(null) } + val options = (state as BookState.Data).booking - if (currentTime.value == null) + + if (currentTime == null) for (el in options) { - currentTime.value = el.key - break + if (!el.value.isEmpty()) { + currentTime = el.key + break + } } - TabGroup(options.keys) - options[currentTime.value]?.let { SelectBooking(it) } + + NavigationBar( + Modifier.fillMaxWidth() + ) { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM") + + options.keys + .map { LocalDate.parse(it, inputFormatter) } + .sorted() +// .map { it.format(inputFormatter) } + .forEachIndexed { index, label -> + options[label.format(inputFormatter)]?.let { + if (!it.isEmpty()) { + NavigationBarItem( + selected = currentTime == label.format(inputFormatter), + onClick = { + currentTime = label.format(inputFormatter) + }, + icon = { + Text( + text = label.format(outputFormatter), + modifier = Modifier + .testTag(TestIds.Book.ITEM_DATE) + ) + }, + modifier = Modifier + .testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + } + } + } + } + + LazyColumn( + Modifier + .fillMaxWidth() + ) { + options[currentTime]?.forEach { book -> + item { + Row( + Modifier + .fillMaxWidth() + .selectableGroup() + .testTag(TestIds.Book.getIdPlaceItemByPosition(book.id)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = book.place, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + RadioButton( + selected = book.id == selectedBooking && currentTime == selectedTime, + onClick = { + selectedBooking = book.id + selectedTime = currentTime + }, + modifier = Modifier + .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) + ) + } + } + } + } Button( onClick = { - viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedBooking.value!!)) + viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedTime!!, selectedBooking!!)) }, modifier = Modifier .testTag(TestIds.Book.BOOK_BUTTON), - enabled = selectedBooking.value != null + enabled = selectedBooking != null ) { Text(stringResource(R.string.to_book)) } @@ -145,64 +216,5 @@ fun BookScreen( } -@Composable -fun TabGroup(options: Set) { - NavigationBar( - Modifier.fillMaxWidth() - ) { - options.forEachIndexed { index, label -> - NavigationBarItem( - selected = currentTime.value == label, - onClick = { - currentTime.value = label - }, - icon = { - Text( - text = label, - modifier = Modifier - .testTag(TestIds.Book.ITEM_DATE) - ) - }, - modifier = Modifier - .testTag(TestIds.Book.getIdDateItemByPosition(index)) - ) - } - } -} -@Composable -fun SelectBooking(options: List) { - LazyColumn( - Modifier - .fillMaxWidth() - ) { - options.forEachIndexed { index, book -> - item { - Row( - Modifier - .fillMaxWidth() - .selectableGroup() - .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = book.place, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_TEXT) - ) - RadioButton( - selected = book == selectedBooking.value && currentTime.value == selectedTime.value, - onClick = { - selectedBooking.value = book - selectedTime.value = currentTime.value - }, - modifier = Modifier - .testTag(TestIds.Book.ITEM_PLACE_SELECTOR) - ) - } - } - } - } -} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt index 1b32a2f..6692699 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -1,11 +1,12 @@ package ru.myitschool.work.ui.screen.book -import ru.myitschool.work.ui.screen.Booking +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.ui.screen.BookingItem sealed interface BookState { object Loading: BookState data class Data( - val booking : Map> = mapOf() + val booking: Map> = mapOf() ): BookState data class Error( val error : String diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index 841bb54..6e3b15a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -14,13 +14,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.BookRepository -import ru.myitschool.work.ui.screen.Booking class BookViewModel : ViewModel() { private val _uiState = MutableStateFlow(BookState.Loading) val uiState: StateFlow = _uiState.asStateFlow(); - private val _navigationFlow = MutableSharedFlow() - val navigationFlow: SharedFlow = _navigationFlow.asSharedFlow() + private val _navigationFlow = MutableSharedFlow(replay = 0, extraBufferCapacity = 1) + val navigationFlow: SharedFlow = _navigationFlow init { loadData() @@ -61,9 +60,10 @@ class BookViewModel : ViewModel() { onSuccess = { it.let { bookings -> _uiState.update { - when (bookings.isEmpty()) { + Log.d("test", bookings.isEmpty().toString()) + when (!bookings.isEmpty()) { true -> BookState.Data( - booking = bookings.toMap() + booking = bookings ) false -> BookState.NotData } @@ -84,14 +84,18 @@ class BookViewModel : ViewModel() { fun onIntent(intent: BookIntent) { when (intent) { is BookIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { _uiState.update { BookState.Loading } - BookRepository.sendData(intent.code, intent.booking).fold( + BookRepository.sendData(intent.code, intent.data, intent.placeId).fold( onSuccess = { - _navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain) + Log.d("send date", "success") + _navigationFlow.emit(BookNavigationEvent.NavigateToMain) }, onFailure = { error -> - BookState.Error(error.message ?: "Неизвестная ошибка") + Log.d("send date", "error: $error") + _uiState.update { + BookState.Error(error.message ?: "Неизвестная ошибка") + } } ) } @@ -111,15 +115,6 @@ class BookViewModel : ViewModel() { } } -fun List.toMap() : Map> { - val options = this - val map : MutableMap> = mutableMapOf() - options.forEach { - if (map[it.date] == null) map[it.date] = mutableListOf(it) - else map[it.date]?.add(it) - } - return map -} sealed interface BookNavigationEvent { object NavigateToAuth : BookNavigationEvent 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..842ddfb --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,102 @@ +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.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.bookingToList() + .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