diff --git a/app/src/main/java/ru/myitschool/work/data/datastore/DataStoreManager.kt b/app/src/main/java/ru/myitschool/work/data/datastore/DataStoreManager.kt index 5ce914b..262260d 100644 --- a/app/src/main/java/ru/myitschool/work/data/datastore/DataStoreManager.kt +++ b/app/src/main/java/ru/myitschool/work/data/datastore/DataStoreManager.kt @@ -15,6 +15,12 @@ class DataStoreManager( private val USER_CODE_KEY = stringPreferencesKey("user_code") } + suspend fun clearUserCode() { + dataStore.edit { preferences -> + preferences.remove(USER_CODE_KEY) + } + } + suspend fun saveUserCode(userCode: UserCode) { dataStore.edit { preferences -> preferences[USER_CODE_KEY] = userCode.code 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..f76836c 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 @@ -4,13 +4,8 @@ import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { - private var codeCache: String? = null suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text - } - } + return NetworkDataSource.checkAuth(text) } } \ 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 new file mode 100644 index 0000000..78995f1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.domain.book.entities.BookingEntity +import ru.myitschool.work.domain.main.entities.UserEntity + +object BookRepository { + + suspend fun loadBooking(text: String): Result { + return NetworkDataSource.loadBooking(text) + } +} \ 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..34f65ac --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.source.NetworkDataSource +import ru.myitschool.work.domain.main.entities.UserEntity + +object MainRepository { + + suspend fun loadData(text: String): Result { + return NetworkDataSource.loadData(text) + } +} \ 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..7a36ca8 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,7 @@ package ru.myitschool.work.data.source 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,6 +12,30 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.domain.book.entities.BookingEntity +import ru.myitschool.work.domain.book.entities.PlaceInfo +import ru.myitschool.work.domain.main.entities.UserEntity + +private const val testJson = """ +{ + "name": "Иванов Петр Федорович", + "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg", + "booking": { + "2025-01-05": {"id":1,"place":"102"}, + "2025-01-06": {"id":2,"place":"209.13"}, + "2025-01-09": {"id":3,"place":"Зона 51. 50"} + } +} +""" + +private const val testBookingJson = """ +{ + "2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}], + "2025-01-06": [{"id": 3, "place": "Зона 51. 50"}], + "2025-01-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}], + "2025-01-08": [{"id": 2, "place": "209.13"}] +} +""" object NetworkDataSource { private val client by lazy { @@ -30,11 +55,45 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) - when (response.status) { - HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) - } + + true // удалить при проверке + +// val response = client.get(getUrl(code, Constants.AUTH_URL)) +// response.status +// when (response.status) { +// HttpStatusCode.OK -> true +// else -> error(response.bodyAsText()) +// } + } + } + + suspend fun loadData(code: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + + Json.decodeFromString(testJson) // удалить при проверке + +// val response = client.get(getUrl(code, Constants.INFO_URL)) +// when (response.status) { +// HttpStatusCode.OK -> { +// response.body() +// } +// else -> error(response.bodyAsText()) +// } + } + } + + suspend fun loadBooking(code: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + + BookingEntity(Json.decodeFromString>>(testBookingJson)) // удалить при проверке + +// val response = client.get(getUrl(code, Constants.BOOKING_URL)) +// when (response.status) { +// HttpStatusCode.OK -> { +// BookingEntity(response.body>>()) +// } +// else -> error(response.bodyAsText()) +// } } } diff --git a/app/src/main/java/ru/myitschool/work/domain/book/LoadBookingUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/book/LoadBookingUseCase.kt new file mode 100644 index 0000000..735a004 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/LoadBookingUseCase.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.domain.book + +import ru.myitschool.work.data.repo.BookRepository +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.domain.book.entities.BookingEntity +import ru.myitschool.work.domain.main.entities.UserEntity + +class LoadBookingUseCase( + private val repository: BookRepository +) { + suspend operator fun invoke( + text: String + ): Result { + return repository.loadBooking(text) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/book/entities/BookingEntity.kt b/app/src/main/java/ru/myitschool/work/domain/book/entities/BookingEntity.kt new file mode 100644 index 0000000..6e31444 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/entities/BookingEntity.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.domain.book.entities + +import kotlinx.serialization.Serializable + +@Serializable +data class BookingEntity( + val bookings: Map> +) diff --git a/app/src/main/java/ru/myitschool/work/domain/book/entities/PlaceInfo.kt b/app/src/main/java/ru/myitschool/work/domain/book/entities/PlaceInfo.kt new file mode 100644 index 0000000..16d2281 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/entities/PlaceInfo.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.book.entities + +import kotlinx.serialization.Serializable + +@Serializable +data class PlaceInfo( + val id: Int, + val place: String +) diff --git a/app/src/main/java/ru/myitschool/work/domain/main/LoadDataUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/main/LoadDataUseCase.kt new file mode 100644 index 0000000..afe9a34 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/LoadDataUseCase.kt @@ -0,0 +1,14 @@ +package ru.myitschool.work.domain.main + +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.domain.main.entities.UserEntity + +class LoadDataUseCase( + private val repository: MainRepository +) { + suspend operator fun invoke( + text: String + ): Result { + return repository.loadData(text) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt b/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt new file mode 100644 index 0000000..15d7ca8 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/entities/BookingInfo.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.main.entities + +import kotlinx.serialization.Serializable + +@Serializable +data class BookingInfo( + val id: Int, + val place: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt new file mode 100644 index 0000000..fd7bfd5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/entities/UserEntity.kt @@ -0,0 +1,26 @@ +package ru.myitschool.work.domain.main.entities + +import kotlinx.serialization.Serializable +import ru.myitschool.work.formatDate + +@Serializable +data class UserEntity( + val name: String, + val photoUrl: String, + val booking: Map? = null +) { + fun getSortedBookings(): List> { + return booking?.entries + ?.sortedBy { (date, _) -> date } + ?.map { it.toPair() } + ?: emptyList() + } + + fun getSortedBookingsWithFormattedDate(): List> { + return getSortedBookings().map { (date, bookingInfo) -> + Triple(date, date.formatDate(), bookingInfo) + } + } + + fun hasBookings(): Boolean = !booking.isNullOrEmpty() +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/Composables.kt b/app/src/main/java/ru/myitschool/work/ui/Composables.kt index e8855fc..a15018d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/Composables.kt +++ b/app/src/main/java/ru/myitschool/work/ui/Composables.kt @@ -1,13 +1,10 @@ package ru.myitschool.work.ui +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope -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.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -15,23 +12,15 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -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.graphics.Color -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ru.myitschool.work.R -import ru.myitschool.work.core.TestIds.Main import ru.myitschool.work.ui.theme.Black -import ru.myitschool.work.ui.theme.Blue import ru.myitschool.work.ui.theme.Gray import ru.myitschool.work.ui.theme.LightBlue import ru.myitschool.work.ui.theme.LightGray @@ -181,6 +170,7 @@ fun BaseText20( @Composable fun BaseButton( + border: BorderStroke? = null, enable: Boolean = true, text: String, btnColor: Color, @@ -190,6 +180,7 @@ fun BaseButton( modifier: Modifier = Modifier.fillMaxWidth() ) { Button( + border = border, enabled = enable, onClick = onClick, colors = ButtonDefaults.buttonColors( @@ -204,33 +195,4 @@ fun BaseButton( icon() BaseText20(text = text) } -} - -@Composable -fun ErrorScreen() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 20.dp, vertical = 40.dp) - ) { - - Spacer(modifier = Modifier.height(80.dp)) - - BaseText24( - text = "Ошибка загрузки данных", - textAlign = TextAlign.Center, - modifier = Modifier - .testTag(Main.ERROR) - .width(250.dp) - ) - - Spacer(modifier = Modifier.height(30.dp)) - - BaseButton( - modifier = Modifier.testTag(Main.REFRESH_BUTTON), - text = "Обновить", - btnColor = Blue, - btnContentColor = White, - onClick = {} - ) - } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt index 6a41bdf..52660b1 100644 --- a/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt +++ b/app/src/main/java/ru/myitschool/work/ui/nav/AuthScreenDestination.kt @@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav import kotlinx.serialization.Serializable @Serializable -data object AuthScreenDestination: ru.myitschool.work.ui.nav.AppDestination \ No newline at end of file +data object AuthScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt index 6f5f601..9a33073 100644 --- a/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt +++ b/app/src/main/java/ru/myitschool/work/ui/nav/BookScreenDestination.kt @@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav import kotlinx.serialization.Serializable @Serializable -data object BookScreenDestination: ru.myitschool.work.ui.nav.AppDestination \ No newline at end of file +data object BookScreenDestination: AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt index f4e2b9e..deca45f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt +++ b/app/src/main/java/ru/myitschool/work/ui/nav/MainScreenDestination.kt @@ -3,6 +3,4 @@ package ru.myitschool.work.ui.nav import kotlinx.serialization.Serializable @Serializable -data object MainScreenDestination: AppDestination { - val userData = "" -} \ No newline at end of file +data object MainScreenDestination: AppDestination \ 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 3b58440..e2a67ad 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 @@ -16,6 +16,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.SplashScreenDestination import ru.myitschool.work.ui.screen.auth.AuthScreen +import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.main.MainScreen import ru.myitschool.work.ui.screen.splash.SplashScreen @@ -41,11 +42,7 @@ fun AppNavHost( MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + BookScreen(navController = navController) } } } \ 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 4fc9a70..8d88685 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 @@ -36,7 +36,7 @@ class AuthViewModel(application: Application) : AndroidViewModel(application) { 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 = { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt new file mode 100644 index 0000000..7633dea --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookAction { + object Auth: BookAction + object Main: BookAction +} \ No newline at end of file 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 new file mode 100644 index 0000000..ad0a6d3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookIntent { + object Back: BookIntent + object LoadBooking: BookIntent +} \ No newline at end of file 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 2b3a552..00c5101 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 @@ -2,91 +2,268 @@ package ru.myitschool.work.ui.screen.book import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator 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.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.myitschool.work.R +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.core.TestIds.Book import ru.myitschool.work.core.TestIds.Main +import ru.myitschool.work.domain.book.entities.BookingEntity +import ru.myitschool.work.formatBookingDate +import ru.myitschool.work.formatDate import ru.myitschool.work.ui.BaseButton import ru.myitschool.work.ui.BaseNoBackgroundButton import ru.myitschool.work.ui.BaseText16 -import ru.myitschool.work.ui.BaseText20 import ru.myitschool.work.ui.BaseText24 -import ru.myitschool.work.ui.nav.BookScreenDestination +import ru.myitschool.work.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.MainScreenDestination +import ru.myitschool.work.ui.screen.main.MainIntent import ru.myitschool.work.ui.theme.Black import ru.myitschool.work.ui.theme.Blue -import ru.myitschool.work.ui.theme.Gray -import ru.myitschool.work.ui.theme.LightGray -import ru.myitschool.work.ui.theme.MontserratFontFamily +import ru.myitschool.work.ui.theme.Typography import ru.myitschool.work.ui.theme.White @Composable fun BookScreen( - navController: NavController + navController: NavController, + viewModel: BookViewModel = viewModel(), ) { - Column( + val state by viewModel.uiState.collectAsState() - ) { - Row( + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { action -> + when(action) { + is BookAction.Auth -> navController.navigate(AuthScreenDestination) - ) { - BaseText24( - text = "Новая встреча" - ) - BaseNoBackgroundButton( - text = "Назад", - onClick = {} + is BookAction.Main -> navController.navigate(MainScreenDestination) + } + } + } + + when(state) { + is BookState.Loading -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp) + ) + } + } + is BookState.Data -> { + DataContent( + viewModel, + bookingData = (state as? BookState.Data)?.userBooking ) } + is BookState.Error -> ErrorContent(viewModel) + is BookState.Empty -> EmptyContent(viewModel) + } +} + +@Composable +fun EmptyContent( + viewModel: BookViewModel +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { Column( - + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(15.dp) + .fillMaxHeight() + .width(320.dp) ) { - BaseText16( - text = "Доступные даты" + + Spacer(modifier = Modifier.height(80.dp)) + + BaseText24( + text = stringResource(R.string.book_all_booked), + modifier = Modifier.testTag(Book.EMPTY), + textAlign = TextAlign.Center ) - BookDateList() - - BaseText16( - text = "Выберите место встречи" - ) - - BookPlaceList() + Spacer(modifier = Modifier.height(20.dp)) BaseButton( - text = "Бронировать", - btnColor = Blue, - btnContentColor = White, - onClick = { navController.navigate(BookScreenDestination)}, + text = stringResource(R.string.book_back), modifier = Modifier - .testTag(Main.ADD_BUTTON) - .padding(horizontal = 10.dp, vertical = 15.dp) - .fillMaxWidth(), - icon = {Image( - painter = painterResource(R.drawable.add_icon), - contentDescription = "plus Icon" - )} - + .fillMaxWidth() + .testTag(Book.BACK_BUTTON), + onClick = { viewModel.onIntent(BookIntent.Back) }, + btnContentColor = White, + btnColor = Blue ) } } } +@Composable +fun ErrorContent( + viewModel: BookViewModel +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(15.dp) + .fillMaxHeight() + .width(320.dp) + ) { + + Spacer(modifier = Modifier.height(80.dp)) + + BaseText24( + text = stringResource(R.string.book_error), + modifier = Modifier.testTag(Book.ERROR), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(20.dp)) + + BaseButton( + border = BorderStroke(1.dp, Blue), + text = stringResource(R.string.book_back), + modifier = Modifier + .fillMaxWidth() + .testTag(Book.BACK_BUTTON), + onClick = { viewModel.onIntent(BookIntent.Back) }, + btnContentColor = Blue, + btnColor = Color.Transparent + ) + + Spacer(modifier = Modifier.height(15.dp)) + + BaseButton( + text = stringResource(R.string.main_update), + modifier = Modifier + .fillMaxWidth() + .testTag(Book.REFRESH_BUTTON), + onClick = { viewModel.onIntent(BookIntent.LoadBooking) }, + btnContentColor = White, + btnColor = Blue + ) + } + } +} + +@Composable +fun DataContent( + viewModel: BookViewModel, + bookingData: BookingEntity? +) { + Column { + Row( + modifier = Modifier + .clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) + .background(Blue) + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 15.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + BaseText24( + text = stringResource(R.string.book_new_book), + color = White, + modifier = Modifier.padding(start = 15.dp) + ) + BaseNoBackgroundButton( + text = stringResource(R.string.book_back), + modifier = Modifier.testTag(Book.BACK_BUTTON), + onClick = { viewModel.onIntent(BookIntent.Back) } + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 20.dp, horizontal = 10.dp) + .clip(RoundedCornerShape(16.dp)) + .background(White) + ) { + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(13.dp) + ) { + Column { + Text( + text = stringResource(R.string.book_available_date), + style = Typography.bodyMedium, + fontSize = 16.sp, + ) + + BookDateList(bookingData?.bookings?.keys?.toList() ?: emptyList()) + + Text( + text = stringResource(R.string.book_choose_place), + style = Typography.bodyMedium, + fontSize = 16.sp, + ) + + BookPlaceList() + } + + BaseButton( + text = stringResource(R.string.booking_button), + btnColor = Blue, + btnContentColor = White, + onClick = { }, + modifier = Modifier + .testTag(Book.BOOK_BUTTON) + .padding(horizontal = 10.dp) + .fillMaxWidth(), + icon = { Image( + painter = painterResource(R.drawable.add_icon), + contentDescription = stringResource(R.string.add_icon_description) + ) } + ) + } + } + } +} + @Composable fun BookPlaceList() { BookPlaceListElement() @@ -98,32 +275,36 @@ fun BookPlaceListElement() { } @Composable -fun BookDateList() { - BookDateListElement() - BookDateListElement() - BookDateListElement() - BookDateListElement() - BookDateListElement() - BookDateListElement() - BookDateListElement() - +fun BookDateList(dates: List) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(7.dp), + modifier = Modifier.padding(vertical = 15.dp) + ) { + dates.forEach { date -> + BookDateListElement(date = date, onClick = { + // Обработка выбора даты + }) + } + } } @Composable -fun BookDateListElement() { +fun BookDateListElement(date: String, onClick: () -> Unit) { Button( - border = BorderStroke(1.dp, Black), - onClick = {}, + contentPadding = PaddingValues(0.dp), modifier = Modifier - .width(60.dp) + .testTag(Book.ITEM_DATE) .padding(0.dp), + border = BorderStroke(1.dp, Black), + onClick = onClick, colors = ButtonColors( contentColor = Black, containerColor = Color.Transparent, disabledContentColor = Black, disabledContainerColor = Color.Transparent), - shape = RoundedCornerShape(16.dp) ) { - BaseText16(text = "16.06") + val formattedDate = formatBookingDate(date) + BaseText16(text = formattedDate) } } \ No newline at end of file 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 new file mode 100644 index 0000000..0235ce1 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.ui.screen.book + +import ru.myitschool.work.domain.book.entities.BookingEntity + +sealed interface BookState { + object Loading: BookState + data class Data(val userBooking: BookingEntity): BookState + object Error: BookState + object Empty: BookState +} \ No newline at end of file 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 new file mode 100644 index 0000000..3b6cd22 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,83 @@ +package ru.myitschool.work.ui.screen.book + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +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.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.myitschool.work.App +import ru.myitschool.work.data.repo.BookRepository +import ru.myitschool.work.domain.book.LoadBookingUseCase +import ru.myitschool.work.ui.screen.main.MainAction +import ru.myitschool.work.ui.screen.main.MainIntent +import kotlin.text.isEmpty + +class BookViewModel(application: Application) : AndroidViewModel(application) { + + private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) } + + private val dataStoreManager by lazy { + (getApplication() as App).dataStoreManager + } + private val _uiState = MutableStateFlow(BookState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + init { + loadBooking() + } + + private fun loadBooking() { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { BookState.Loading } + + try { + val userCode = dataStoreManager.getUserCode().first() + + if (userCode.code.isEmpty()) { + _actionFlow.emit(BookAction.Auth) + return@launch + } + + loadBookingUseCase.invoke(userCode.code).fold( + onSuccess = { data -> + if (data.bookings.isEmpty()) { + _uiState.update { BookState.Empty } + } + else { + _uiState.update { BookState.Data(data) } + } + }, + onFailure = { error -> + error.printStackTrace() + _uiState.update { BookState.Error } + } + ) + } catch (error: Exception) { + error.printStackTrace() + _uiState.update { BookState.Error } + } + } + } + + fun onIntent( intent: BookIntent) { + when(intent) { + is BookIntent.LoadBooking -> loadBooking() + + is BookIntent.Back -> { + viewModelScope.launch(Dispatchers.Default) { + _actionFlow.emit(BookAction.Main) + } + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt new file mode 100644 index 0000000..0a59f7e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainAction { + object Booking: MainAction + object Auth: MainAction +} \ 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..c324687 --- /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 { + object Logout: MainIntent + object Booking: MainIntent + object LoadData: 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 index fee50ff..68f24fb 100644 --- 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 @@ -7,34 +7,47 @@ 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.fillMaxHeight 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.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator 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.runtime.remember 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.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import coil3.compose.rememberAsyncImagePainter import ru.myitschool.work.R import ru.myitschool.work.core.TestIds.Main +import ru.myitschool.work.domain.main.entities.BookingInfo +import ru.myitschool.work.domain.main.entities.UserEntity import ru.myitschool.work.ui.BaseButton import ru.myitschool.work.ui.BaseNoBackgroundButton import ru.myitschool.work.ui.BaseText14 +import ru.myitschool.work.ui.BaseText16 import ru.myitschool.work.ui.BaseText20 +import ru.myitschool.work.ui.BaseText24 +import ru.myitschool.work.ui.nav.AuthScreenDestination +import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.theme.Black import ru.myitschool.work.ui.theme.Blue import ru.myitschool.work.ui.theme.LightGray @@ -49,8 +62,19 @@ fun MainScreen( val state by viewModel.uiState.collectAsState() + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { action -> + when(action) { + is MainAction.Auth -> navController.navigate(AuthScreenDestination) + + is MainAction.Booking -> navController.navigate(BookScreenDestination) + } + } + } + when(state) { is MainState.Loading -> { + Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize() @@ -61,16 +85,59 @@ fun MainScreen( } } is MainState.Error -> { - + ErrorContent(viewModel) } is MainState.Data -> { - Content(viewModel) + DataContent( + viewModel, + userData = (state as MainState.Data).userData + ) } } } @Composable -fun Content(viewModel: MainViewModel) { +fun ErrorContent(viewModel: MainViewModel){ + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(15.dp) + .fillMaxHeight() + .width(320.dp) + ) { + + Spacer(modifier = Modifier.height(80.dp)) + + BaseText24( + text = stringResource(R.string.data_error_message), + modifier = Modifier.testTag(Main.ERROR), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(20.dp)) + + BaseButton( + text = stringResource(R.string.main_update), + modifier = Modifier + .fillMaxWidth() + .testTag(Main.REFRESH_BUTTON), + onClick = { viewModel.onIntent(MainIntent.LoadData) }, + btnContentColor = White, + btnColor = Blue + ) + } + } +} + +@Composable +fun DataContent( + viewModel: MainViewModel, + userData: UserEntity +) { Column ( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -92,27 +159,33 @@ fun Content(viewModel: MainViewModel) { modifier = Modifier.fillMaxWidth() ) { BaseNoBackgroundButton( - text = "Обновить", - onClick = { }, + text = stringResource(R.string.main_update), + onClick = { viewModel.onIntent(MainIntent.LoadData) }, modifier = Modifier.testTag(Main.REFRESH_BUTTON) ) BaseNoBackgroundButton( - text = "Выйти", - onClick = { }, + text = stringResource(R.string.main_log_out), + onClick = { viewModel.onIntent(MainIntent.Logout) }, modifier = Modifier.testTag(Main.LOGOUT_BUTTON) ) } Image( - painter = painterResource(R.drawable.avatar), - contentDescription = "User avatar", + painter = rememberAsyncImagePainter( + model = userData.photoUrl, + error = painterResource(R.drawable.avatar) + ), + contentDescription = stringResource(R.string.main_avatar_description), modifier = Modifier + .clip(RoundedCornerShape(999.dp)) .testTag(Main.PROFILE_IMAGE) + .width(150.dp) + .height(150.dp) .padding(20.dp) ) BaseText20( - text = "Артемий Артемиев Иванович", + text = userData.name, color = White, textAlign = TextAlign.Center, modifier = Modifier @@ -135,7 +208,7 @@ fun Content(viewModel: MainViewModel) { modifier = Modifier.fillMaxWidth() ) { Text( - text = "Ваши забронированные места", + text = stringResource(R.string.main_booking_title), style = Typography.bodyMedium, color = Black, fontSize = 16.sp, @@ -144,20 +217,24 @@ fun Content(viewModel: MainViewModel) { vertical = 20.dp ) ) - BookList() + if (userData.hasBookings()) { + SortedBookingList(userData = userData) + } else { + EmptyBookings() + } } BaseButton( - text = "Бронировать", + text = stringResource(R.string.booking_button), btnColor = Blue, btnContentColor = White, - onClick = {}, + onClick = { viewModel.onIntent(MainIntent.Booking) }, modifier = Modifier .testTag(Main.ADD_BUTTON) .padding(horizontal = 10.dp, vertical = 15.dp) .fillMaxWidth(), icon = {Image( painter = painterResource(R.drawable.add_icon), - contentDescription = "plus Icon" + contentDescription = stringResource(R.string.add_icon_description) )} ) @@ -165,25 +242,38 @@ fun Content(viewModel: MainViewModel) { } } -val bookListData = listOf( - "Конгресс Холл", - "Конгресс Холл", - "Конгресс Холл" -) - @Composable -fun BookList() { - Column( - modifier = Modifier.padding(horizontal = 20.dp) +fun SortedBookingList(userData: UserEntity) { + val sortedBookings = remember(userData.booking) { + userData.getSortedBookingsWithFormattedDate() + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) ) { - for ((index, book) in bookListData.withIndex()) { - BookListElement(index) + itemsIndexed( + items = sortedBookings + ) { index, (originalDate, formattedDate, bookingInfo) -> + BookingItem( + originalDate = originalDate, + formattedDate = formattedDate, + bookingInfo = bookingInfo, + index = index + ) + Spacer(modifier = Modifier.height(8.dp)) } } } @Composable -fun BookListElement(index: Int) { +fun BookingItem( + originalDate: String, + formattedDate: String, + bookingInfo: BookingInfo, + index: Int +) { Row( modifier = Modifier .testTag(Main.getIdItemByPosition(index)) @@ -192,12 +282,26 @@ fun BookListElement(index: Int) { horizontalArrangement = Arrangement.SpaceBetween ) { BaseText14( - text = "Конгресс Холл", + text = bookingInfo.place, modifier = Modifier.testTag(Main.ITEM_PLACE) ) BaseText14( - text = "16.02.3026", + text = formattedDate, modifier = Modifier.testTag(Main.ITEM_DATE) ) } +} + +@Composable +fun EmptyBookings() { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + BaseText16( + text = stringResource(R.string.main_empty_booking) + ) + } } \ 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 index 067cf84..285be1f 100644 --- 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 @@ -1,7 +1,9 @@ package ru.myitschool.work.ui.screen.main +import ru.myitschool.work.domain.main.entities.UserEntity + sealed interface MainState { - object Data: MainState + data class Data(val userData: UserEntity): MainState object Loading: MainState object Error: 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 index 76869d7..381d3a9 100644 --- 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 @@ -1,15 +1,82 @@ package ru.myitschool.work.ui.screen.main -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel +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.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.myitschool.work.App +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.domain.main.LoadDataUseCase -class MainViewModel: ViewModel() { +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val dataStoreManager by lazy { + (getApplication() as App).dataStoreManager + } + + private val loadDataUseCase by lazy { LoadDataUseCase(MainRepository) } private val _uiState = MutableStateFlow(MainState.Loading) val uiState: StateFlow = _uiState.asStateFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + init { + loadData() + } + private fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { MainState.Loading } + try { + val userCode = dataStoreManager.getUserCode().first() + + if (userCode.code.isEmpty()) { + _actionFlow.emit(MainAction.Auth) + return@launch + } + + loadDataUseCase.invoke(userCode.code).fold( + onSuccess = { data -> + _uiState.update { MainState.Data(data) } + }, + onFailure = { error -> + error.printStackTrace() + _uiState.update { MainState.Error } + } + ) + } catch (error: Exception) { + error.printStackTrace() + _uiState.update { MainState.Error } + } + } + } + + fun onIntent( intent: MainIntent) { + when(intent) { + is MainIntent.LoadData -> loadData() + + is MainIntent.Booking -> { + viewModelScope.launch(Dispatchers.Default) { + _actionFlow.emit(MainAction.Booking) + } + } + + is MainIntent.Logout -> { + viewModelScope.launch(Dispatchers.IO) { + + dataStoreManager.clearUserCode() + _actionFlow.emit(MainAction.Auth) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashViewModel.kt index 4b419b4..b7d2ecd 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/splash/SplashViewModel.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.screen.splash import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -28,12 +29,7 @@ class SplashViewModel(application: Application) : AndroidViewModel(application) try { val userCode = dataStoreManager.getUserCode().first() - val isAuthenticated = when { - userCode == null -> false - userCode.code is String -> (userCode.code as String).isNotEmpty() - userCode.code is Int -> (userCode.code as Int) != -1 - else -> false - } + val isAuthenticated = if (userCode.code.isEmpty()) false else true _splashState.value = if (isAuthenticated) { SplashState.Authenticated diff --git a/app/src/main/java/ru/myitschool/work/utils.kt b/app/src/main/java/ru/myitschool/work/utils.kt new file mode 100644 index 0000000..edd2a78 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils.kt @@ -0,0 +1,25 @@ +package ru.myitschool.work + +fun String.formatDate(): String { + return try { + val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) + val outputFormat = java.text.SimpleDateFormat("dd.MM.yyyy", java.util.Locale.getDefault()) + val date = inputFormat.parse(this) + outputFormat.format(date) + } catch (e: Exception) { + this + } +} + +fun formatBookingDate(dateString: String): String { + return try { + val parts = dateString.split("-") + if (parts.size == 3) { + "${parts[2]}.${parts[1]}" + } else { + dateString + } + } catch (e: Exception) { + dateString + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d9daf4..d59bbf0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,18 @@ Введите код для авторизации Код Войти + Обновить + Выйти + Фото пользователя + Ваши забронированные места + Бронировать + Иконка добавления + Ошибка загрузки данных + Нет бронирований + Новая встреча + Назад + Доступные даты + Выберите место встречи + Всё забронировано + Ошибка сервера \ No newline at end of file