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 78995f1..bfdc8c3 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 @@ -9,4 +9,13 @@ object BookRepository { suspend fun loadBooking(text: String): Result { return NetworkDataSource.loadBooking(text) } + + suspend fun bookPlace( + userCode: String, + date: String, + placeId: Int, + placeName: String + ): Result { + return NetworkDataSource.bookPlace(userCode, date, placeId, placeName) + } } \ 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 7a36ca8..70a66cc 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,10 +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.HttpStatusCode import io.ktor.serialization.kotlinx.json.json @@ -53,47 +56,72 @@ object NetworkDataSource { } } + suspend fun bookPlace( + userCode: String, + date: String, + placeId: Int, + placeName: String + ): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { +// Log.i("aaa", "Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName") +// println("Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName") + + val response = client.post(getUrl(userCode, Constants.BOOK_URL)) { + setBody(mapOf( + "date" to date, + "placeId" to placeId, + "placeName" to placeName + )) + } + + when (response.status) { + HttpStatusCode.OK -> Unit + else -> error(response.bodyAsText()) + } + } + } + suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - true // удалить при проверке + // true // удалить при проверке -// val response = client.get(getUrl(code, Constants.AUTH_URL)) -// response.status -// when (response.status) { -// HttpStatusCode.OK -> true -// else -> error(response.bodyAsText()) -// } + 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) // удалить при проверке +// Json.decodeFromString(testJson) // удалить при проверке -// val response = client.get(getUrl(code, Constants.INFO_URL)) -// when (response.status) { -// HttpStatusCode.OK -> { -// response.body() -// } -// else -> error(response.bodyAsText()) -// } + 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)) // удалить при проверке +// 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()) -// } + 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/BookingUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/book/BookingUseCase.kt new file mode 100644 index 0000000..cd06657 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/BookingUseCase.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.domain.book + +import ru.myitschool.work.data.repo.BookRepository + +class BookingUseCase( + private val repository: BookRepository +) { + suspend operator fun invoke( + userCode: String, + date: String, + placeId: Int, + placeName: String + ): Result { + return repository.bookPlace(userCode, date, placeId, placeName) + } +} \ 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 8d88685..5a142ac 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 @@ -46,7 +46,7 @@ class AuthViewModel(application: Application) : AndroidViewModel(application) { onFailure = { error -> error.printStackTrace() _uiState.update { AuthState.Data } - _errorStateValue.value = "Неизвестная ошибка" + _errorStateValue.value = error.message.toString() ?: "Неизвестная ошибка" } ) } 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 ad0a6d3..c9b8c05 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 @@ -3,4 +3,10 @@ package ru.myitschool.work.ui.screen.book sealed interface BookIntent { object Back: BookIntent object LoadBooking: BookIntent + object Book : BookIntent + data class SelectDate(val date: String) : BookIntent + data class SelectPlace( + val placeId: Int, + val placeName: String + ) : 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 00c5101..2fae136 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 @@ -1,310 +1,424 @@ -package ru.myitschool.work.ui.screen.book + 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.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.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.BaseText24 -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.Typography -import ru.myitschool.work.ui.theme.White + import androidx.compose.foundation.BorderStroke + import androidx.compose.foundation.Image + import androidx.compose.foundation.background + import androidx.compose.foundation.border + 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.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.selection.selectable + import androidx.compose.foundation.shape.CircleShape + import androidx.compose.foundation.shape.RoundedCornerShape + import androidx.compose.material3.Button + import androidx.compose.material3.ButtonColors + 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.domain.book.entities.PlaceInfo + 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.BaseText24 + 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.Typography + import ru.myitschool.work.ui.theme.White -@Composable -fun BookScreen( - navController: NavController, - viewModel: BookViewModel = viewModel(), -) { - val state by viewModel.uiState.collectAsState() + @Composable + fun BookScreen( + navController: NavController, + viewModel: BookViewModel = viewModel(), + ) { + val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { action -> - when(action) { - is BookAction.Auth -> navController.navigate(AuthScreenDestination) + LaunchedEffect(Unit) { + viewModel.actionFlow.collect { action -> + when(action) { + is BookAction.Auth -> navController.navigate(AuthScreenDestination) - is BookAction.Main -> navController.navigate(MainScreenDestination) + is BookAction.Main -> navController.navigate(MainScreenDestination) + } } } - } - when(state) { - is BookState.Loading -> { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) + when(state) { + is BookState.Loading -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier.size(64.dp) + ) + } + } + is BookState.Data -> { + val dataState = state as BookState.Data + DataContent( + viewModel = viewModel, + bookingData = dataState.userBooking, + selectedDate = dataState.selectedDate, + selectedPlaceId = dataState.selectedPlaceId ) } + is BookState.Error -> ErrorContent(viewModel) + is BookState.Empty -> EmptyContent(viewModel) } - 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() + @Composable + fun EmptyContent( + viewModel: BookViewModel ) { - 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_all_booked), - modifier = Modifier.testTag(Book.EMPTY), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(20.dp)) - - BaseButton( - text = stringResource(R.string.book_back), - modifier = Modifier - .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) + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() ) { Column( - verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .fillMaxSize() - .padding(13.dp) + .padding(15.dp) + .fillMaxHeight() + .width(320.dp) ) { - Column { - Text( - text = stringResource(R.string.book_available_date), - style = Typography.bodyMedium, - fontSize = 16.sp, - ) - BookDateList(bookingData?.bookings?.keys?.toList() ?: emptyList()) + Spacer(modifier = Modifier.height(80.dp)) - Text( - text = stringResource(R.string.book_choose_place), - style = Typography.bodyMedium, - fontSize = 16.sp, - ) + BaseText24( + text = stringResource(R.string.book_all_booked), + modifier = Modifier.testTag(Book.EMPTY), + textAlign = TextAlign.Center + ) - BookPlaceList() - } + Spacer(modifier = Modifier.height(20.dp)) BaseButton( - text = stringResource(R.string.booking_button), - btnColor = Blue, - btnContentColor = White, - onClick = { }, + text = stringResource(R.string.book_back), 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) - ) } + .fillMaxWidth() + .testTag(Book.BACK_BUTTON), + onClick = { viewModel.onIntent(BookIntent.Back) }, + btnContentColor = White, + btnColor = Blue ) } } } -} -@Composable -fun BookPlaceList() { - BookPlaceListElement() -} - -@Composable -fun BookPlaceListElement() { - -} - -@Composable -fun BookDateList(dates: List) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(7.dp), - modifier = Modifier.padding(vertical = 15.dp) + @Composable + fun ErrorContent( + viewModel: BookViewModel ) { - dates.forEach { date -> - BookDateListElement(date = date, onClick = { - // Обработка выбора даты - }) + 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 BookDateListElement(date: String, onClick: () -> Unit) { - Button( - contentPadding = PaddingValues(0.dp), - modifier = Modifier - .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), + @Composable + fun DataContent( + viewModel: BookViewModel, + bookingData: BookingEntity, + selectedDate: String, + selectedPlaceId: Int ) { - val formattedDate = formatBookingDate(date) - BaseText16(text = formattedDate) + + val availableDates = bookingData.bookings + .filter { it.value.isNotEmpty() } + .keys + .sorted() + val placesForSelectedDate = bookingData.bookings[selectedDate] ?: emptyList() + + 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( + dates = availableDates, + selectedDate = selectedDate, + onDateSelected = { date -> + viewModel.onIntent(BookIntent.SelectDate(date)) + } + ) + + Text( + text = stringResource(R.string.book_choose_place), + style = Typography.bodyMedium, + fontSize = 16.sp, + ) + + BookPlaceList( + places = placesForSelectedDate, + selectedPlaceId = selectedPlaceId, + onPlaceSelected = { placeId, placeName -> + viewModel.onIntent(BookIntent.SelectPlace(placeId, placeName)) + } + ) + } + + BaseButton( + enable = selectedPlaceId != -1, + text = stringResource(R.string.booking_button), + btnColor = Blue, + btnContentColor = White, + onClick = { viewModel.onIntent(BookIntent.Book) }, + 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) + ) } + ) + } + } + } } -} \ No newline at end of file + + @Composable + fun BookPlaceList( + places: List, + selectedPlaceId: Int, + onPlaceSelected: (Int, String) -> Unit + ) { + Column( + modifier = Modifier.padding(vertical = 15.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (places.isEmpty()) { + Text( + text = "Нет доступных мест для выбранной даты", + color = Color.Gray, + style = Typography.bodyMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } else { + places.forEachIndexed { index, placeInfo -> + BookPlaceListElement( + placeInfo = placeInfo, + isSelected = placeInfo.id == selectedPlaceId, + onPlaceSelected = { onPlaceSelected(placeInfo.id, placeInfo.place) }, + index = index + ) + } + } + } + } + + @Composable + fun BookPlaceListElement( + placeInfo: PlaceInfo, + isSelected: Boolean, + onPlaceSelected: () -> Unit, + index: Int + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = onPlaceSelected + ) + .testTag(Book.getIdPlaceItemByPosition(index)) + .padding(vertical = 12.dp, horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + BaseText16( + text = placeInfo.place, + modifier = Modifier.testTag(Book.ITEM_PLACE_TEXT) + ) + Box( + modifier = Modifier + .size(24.dp) + .border( + width = 2.dp, + color = if (isSelected) Blue else Color.Gray, + shape = CircleShape + ) + .background( + color = if (isSelected) Blue else Color.Transparent, + shape = CircleShape + ) + .testTag(Book.ITEM_PLACE_SELECTOR) + ) { + if (isSelected) { + Box( + modifier = Modifier + .size(12.dp) + .background(Color.White, CircleShape) + .align(Alignment.Center) + ) + } + } + } + } + + @Composable + fun BookDateList( + dates: List, + selectedDate: String, + onDateSelected: (String) -> Unit + ) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(7.dp), + modifier = Modifier.padding(vertical = 15.dp) + ) { + dates.forEachIndexed { index, date -> + BookDateListElement( + date = date, + isSelected = date == selectedDate, + onClick = { onDateSelected(date) }, + index = index + ) + } + } + } + + @Composable + fun BookDateListElement( + date: String, + isSelected: Boolean, + onClick: () -> Unit, + index: Int + ) { + Button( + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .testTag(Book.getIdDateItemByPosition(index)) + .padding(0.dp), + border = BorderStroke(1.dp, if (isSelected) Blue else Black,), + onClick = onClick, + colors = ButtonColors( + contentColor = if (isSelected) White else Black, + containerColor = if (isSelected) Blue else Color.Transparent, + disabledContentColor = Black, + disabledContainerColor = Color.Transparent), + ) { + val formattedDate = date.formatBookingDate() + BaseText16( + text = formattedDate, + modifier = Modifier.testTag(Book.ITEM_DATE), + color = if (isSelected) White else Black, + ) + } + } \ 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 index 0235ce1..856dccf 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 @@ -4,7 +4,12 @@ import ru.myitschool.work.domain.book.entities.BookingEntity sealed interface BookState { object Loading: BookState - data class Data(val userBooking: BookingEntity): BookState + data class Data( + val userBooking: BookingEntity, + val selectedDate: String = "", + val selectedPlaceId: Int = -1, + val selectedPlaceName: String = "" + ): 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 index 3b6cd22..84dd63f 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,15 +14,21 @@ 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.data.repo.MainRepository +import ru.myitschool.work.domain.book.BookingUseCase import ru.myitschool.work.domain.book.LoadBookingUseCase +import ru.myitschool.work.domain.main.LoadDataUseCase import ru.myitschool.work.ui.screen.main.MainAction import ru.myitschool.work.ui.screen.main.MainIntent +import ru.myitschool.work.ui.screen.main.MainState import kotlin.text.isEmpty class BookViewModel(application: Application) : AndroidViewModel(application) { private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) } + private val bookingUseCase by lazy { BookingUseCase (BookRepository) } + private val dataStoreManager by lazy { (getApplication() as App).dataStoreManager } @@ -35,6 +41,35 @@ class BookViewModel(application: Application) : AndroidViewModel(application) { loadBooking() } + private fun bookSelectedPlace() { + viewModelScope.launch(Dispatchers.IO) { + try { + val userCode = dataStoreManager.getUserCode().first() + val currentState = _uiState.value + + if (currentState is BookState.Data && currentState.selectedPlaceId != -1) { + bookingUseCase.invoke( + userCode = userCode.code, + date = currentState.selectedDate, + placeId = currentState.selectedPlaceId, + placeName = currentState.selectedPlaceName + ).fold( + onSuccess = { + _actionFlow.emit(BookAction.Main) + }, + onFailure = { error -> + error.printStackTrace() + _uiState.update { BookState.Error } + } + ) + } + } catch (error: Exception) { + error.printStackTrace() + _uiState.update { BookState.Error } + } + } + } + private fun loadBooking() { viewModelScope.launch(Dispatchers.IO) { _uiState.update { BookState.Loading } @@ -49,11 +84,27 @@ class BookViewModel(application: Application) : AndroidViewModel(application) { loadBookingUseCase.invoke(userCode.code).fold( onSuccess = { data -> - if (data.bookings.isEmpty()) { + val availableDates = data.bookings + .filter { it.value.isNotEmpty() } + .keys + .sorted() + + if (availableDates.isEmpty()) { _uiState.update { BookState.Empty } - } - else { - _uiState.update { BookState.Data(data) } + } else { + val selectedDate = availableDates.first() + val placesForSelectedDate = data.bookings[selectedDate] ?: emptyList() + val selectedPlaceId = placesForSelectedDate.firstOrNull()?.id ?: -1 + val selectedPlaceName = placesForSelectedDate.firstOrNull()?.place ?: "" + + _uiState.update { + BookState.Data( + userBooking = data, + selectedDate = selectedDate, + selectedPlaceId = selectedPlaceId, + selectedPlaceName = selectedPlaceName + ) + } } }, onFailure = { error -> @@ -68,8 +119,8 @@ class BookViewModel(application: Application) : AndroidViewModel(application) { } } - fun onIntent( intent: BookIntent) { - when(intent) { + fun onIntent(intent: BookIntent) { + when (intent) { is BookIntent.LoadBooking -> loadBooking() is BookIntent.Back -> { @@ -78,6 +129,37 @@ class BookViewModel(application: Application) : AndroidViewModel(application) { } } + is BookIntent.Book -> bookSelectedPlace() + + is BookIntent.SelectDate -> { + val currentState = _uiState.value + if (currentState is BookState.Data) { + val placesForDate = + currentState.userBooking.bookings[intent.date] ?: emptyList() + val newSelectedPlaceId = placesForDate.firstOrNull()?.id ?: -1 + val newSelectedPlaceName = placesForDate.firstOrNull()?.place ?: "" + + _uiState.update { + currentState.copy( + selectedDate = intent.date, + selectedPlaceId = newSelectedPlaceId, + selectedPlaceName = newSelectedPlaceName + ) + } + } + } + + is BookIntent.SelectPlace -> { + val currentState = _uiState.value + if (currentState is BookState.Data) { + _uiState.update { + currentState.copy( + selectedPlaceId = intent.placeId, + selectedPlaceName = intent.placeName + ) + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils.kt b/app/src/main/java/ru/myitschool/work/utils.kt index edd2a78..88e89e4 100644 --- a/app/src/main/java/ru/myitschool/work/utils.kt +++ b/app/src/main/java/ru/myitschool/work/utils.kt @@ -1,9 +1,12 @@ package ru.myitschool.work +import java.text.SimpleDateFormat +import java.util.Locale + 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 inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val outputFormat = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) val date = inputFormat.parse(this) outputFormat.format(date) } catch (e: Exception) { @@ -11,15 +14,13 @@ fun String.formatDate(): String { } } -fun formatBookingDate(dateString: String): String { +fun String.formatBookingDate(): String { return try { - val parts = dateString.split("-") - if (parts.size == 3) { - "${parts[2]}.${parts[1]}" - } else { - dateString - } + val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault()) + val date = inputFormat.parse(this) + outputFormat.format(date) } catch (e: Exception) { - dateString + this } } \ No newline at end of file