add full logic to BookScreen

This commit is contained in:
2025-12-03 22:52:16 +03:00
parent 4e45459af2
commit 75ffc79666
9 changed files with 577 additions and 316 deletions

View File

@@ -9,4 +9,13 @@ object BookRepository {
suspend fun loadBooking(text: String): Result<BookingEntity> { suspend fun loadBooking(text: String): Result<BookingEntity> {
return NetworkDataSource.loadBooking(text) return NetworkDataSource.loadBooking(text)
} }
suspend fun bookPlace(
userCode: String,
date: String,
placeId: Int,
placeName: String
): Result<Unit> {
return NetworkDataSource.bookPlace(userCode, date, placeId, placeName)
}
} }

View File

@@ -1,10 +1,13 @@
package ru.myitschool.work.data.source package ru.myitschool.work.data.source
import android.util.Log
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json 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<Unit> = 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<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
true // удалить при проверке // true // удалить при проверке
// val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.get(getUrl(code, Constants.AUTH_URL))
// response.status response.status
// when (response.status) { when (response.status) {
// HttpStatusCode.OK -> true HttpStatusCode.OK -> true
// else -> error(response.bodyAsText()) else -> error(response.bodyAsText())
// } }
} }
} }
suspend fun loadData(code: String): Result<UserEntity> = withContext(Dispatchers.IO) { suspend fun loadData(code: String): Result<UserEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
Json.decodeFromString<UserEntity>(testJson) // удалить при проверке // Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
// val response = client.get(getUrl(code, Constants.INFO_URL)) val response = client.get(getUrl(code, Constants.INFO_URL))
// when (response.status) { when (response.status) {
// HttpStatusCode.OK -> { HttpStatusCode.OK -> {
// response.body<UserEntity>() response.body<UserEntity>()
// } }
// else -> error(response.bodyAsText()) else -> error(response.bodyAsText())
// } }
} }
} }
suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) { suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
BookingEntity(Json.decodeFromString<Map<String, List<PlaceInfo>>>(testBookingJson)) // удалить при проверке // BookingEntity(Json.decodeFromString<Map<String, List<PlaceInfo>>>(testBookingJson)) // удалить при проверке
// val response = client.get(getUrl(code, Constants.BOOKING_URL)) val response = client.get(getUrl(code, Constants.BOOKING_URL))
// when (response.status) { when (response.status) {
// HttpStatusCode.OK -> { HttpStatusCode.OK -> {
// BookingEntity(response.body<Map<String, List<PlaceInfo>>>()) BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
// } }
// else -> error(response.bodyAsText()) else -> error(response.bodyAsText())
// } }
} }
} }

View File

@@ -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<Unit> {
return repository.bookPlace(userCode, date, placeId, placeName)
}
}

View File

@@ -46,7 +46,7 @@ class AuthViewModel(application: Application) : AndroidViewModel(application) {
onFailure = { error -> onFailure = { error ->
error.printStackTrace() error.printStackTrace()
_uiState.update { AuthState.Data } _uiState.update { AuthState.Data }
_errorStateValue.value = "Неизвестная ошибка" _errorStateValue.value = error.message.toString() ?: "Неизвестная ошибка"
} }
) )
} }

View File

@@ -3,4 +3,10 @@ package ru.myitschool.work.ui.screen.book
sealed interface BookIntent { sealed interface BookIntent {
object Back: BookIntent object Back: BookIntent
object LoadBooking: BookIntent object LoadBooking: BookIntent
object Book : BookIntent
data class SelectDate(val date: String) : BookIntent
data class SelectPlace(
val placeId: Int,
val placeName: String
) : BookIntent
} }

View File

@@ -3,6 +3,7 @@ package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -17,6 +18,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
@@ -43,6 +46,7 @@ import ru.myitschool.work.core.TestIds
import ru.myitschool.work.core.TestIds.Book import ru.myitschool.work.core.TestIds.Book
import ru.myitschool.work.core.TestIds.Main import ru.myitschool.work.core.TestIds.Main
import ru.myitschool.work.domain.book.entities.BookingEntity 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.formatBookingDate
import ru.myitschool.work.formatDate import ru.myitschool.work.formatDate
import ru.myitschool.work.ui.BaseButton import ru.myitschool.work.ui.BaseButton
@@ -86,9 +90,12 @@ fun BookScreen(
} }
} }
is BookState.Data -> { is BookState.Data -> {
val dataState = state as BookState.Data
DataContent( DataContent(
viewModel, viewModel = viewModel,
bookingData = (state as? BookState.Data)?.userBooking bookingData = dataState.userBooking,
selectedDate = dataState.selectedDate,
selectedPlaceId = dataState.selectedPlaceId
) )
} }
is BookState.Error -> ErrorContent(viewModel) is BookState.Error -> ErrorContent(viewModel)
@@ -190,8 +197,17 @@ fun ErrorContent(
@Composable @Composable
fun DataContent( fun DataContent(
viewModel: BookViewModel, viewModel: BookViewModel,
bookingData: BookingEntity? bookingData: BookingEntity,
selectedDate: String,
selectedPlaceId: Int
) { ) {
val availableDates = bookingData.bookings
.filter { it.value.isNotEmpty() }
.keys
.sorted()
val placesForSelectedDate = bookingData.bookings[selectedDate] ?: emptyList()
Column { Column {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -234,7 +250,13 @@ fun DataContent(
fontSize = 16.sp, fontSize = 16.sp,
) )
BookDateList(bookingData?.bookings?.keys?.toList() ?: emptyList()) BookDateList(
dates = availableDates,
selectedDate = selectedDate,
onDateSelected = { date ->
viewModel.onIntent(BookIntent.SelectDate(date))
}
)
Text( Text(
text = stringResource(R.string.book_choose_place), text = stringResource(R.string.book_choose_place),
@@ -242,14 +264,21 @@ fun DataContent(
fontSize = 16.sp, fontSize = 16.sp,
) )
BookPlaceList() BookPlaceList(
places = placesForSelectedDate,
selectedPlaceId = selectedPlaceId,
onPlaceSelected = { placeId, placeName ->
viewModel.onIntent(BookIntent.SelectPlace(placeId, placeName))
}
)
} }
BaseButton( BaseButton(
enable = selectedPlaceId != -1,
text = stringResource(R.string.booking_button), text = stringResource(R.string.booking_button),
btnColor = Blue, btnColor = Blue,
btnContentColor = White, btnContentColor = White,
onClick = { }, onClick = { viewModel.onIntent(BookIntent.Book) },
modifier = Modifier modifier = Modifier
.testTag(Book.BOOK_BUTTON) .testTag(Book.BOOK_BUTTON)
.padding(horizontal = 10.dp) .padding(horizontal = 10.dp)
@@ -265,46 +294,131 @@ fun DataContent(
} }
@Composable @Composable
fun BookPlaceList() { fun BookPlaceList(
BookPlaceListElement() places: List<PlaceInfo>,
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 @Composable
fun BookPlaceListElement() { 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 @Composable
fun BookDateList(dates: List<String>) { fun BookDateList(
dates: List<String>,
selectedDate: String,
onDateSelected: (String) -> Unit
) {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(7.dp), verticalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier.padding(vertical = 15.dp) modifier = Modifier.padding(vertical = 15.dp)
) { ) {
dates.forEach { date -> dates.forEachIndexed { index, date ->
BookDateListElement(date = date, onClick = { BookDateListElement(
// Обработка выбора даты date = date,
}) isSelected = date == selectedDate,
onClick = { onDateSelected(date) },
index = index
)
} }
} }
} }
@Composable @Composable
fun BookDateListElement(date: String, onClick: () -> Unit) { fun BookDateListElement(
date: String,
isSelected: Boolean,
onClick: () -> Unit,
index: Int
) {
Button( Button(
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
modifier = Modifier modifier = Modifier
.testTag(Book.ITEM_DATE) .testTag(Book.getIdDateItemByPosition(index))
.padding(0.dp), .padding(0.dp),
border = BorderStroke(1.dp, Black), border = BorderStroke(1.dp, if (isSelected) Blue else Black,),
onClick = onClick, onClick = onClick,
colors = ButtonColors( colors = ButtonColors(
contentColor = Black, contentColor = if (isSelected) White else Black,
containerColor = Color.Transparent, containerColor = if (isSelected) Blue else Color.Transparent,
disabledContentColor = Black, disabledContentColor = Black,
disabledContainerColor = Color.Transparent), disabledContainerColor = Color.Transparent),
) { ) {
val formattedDate = formatBookingDate(date) val formattedDate = date.formatBookingDate()
BaseText16(text = formattedDate) BaseText16(
text = formattedDate,
modifier = Modifier.testTag(Book.ITEM_DATE),
color = if (isSelected) White else Black,
)
} }
} }

View File

@@ -4,7 +4,12 @@ import ru.myitschool.work.domain.book.entities.BookingEntity
sealed interface BookState { sealed interface BookState {
object Loading: 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 Error: BookState
object Empty: BookState object Empty: BookState
} }

View File

@@ -14,15 +14,21 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.App import ru.myitschool.work.App
import ru.myitschool.work.data.repo.BookRepository 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.book.LoadBookingUseCase
import ru.myitschool.work.domain.main.LoadDataUseCase
import ru.myitschool.work.ui.screen.main.MainAction import ru.myitschool.work.ui.screen.main.MainAction
import ru.myitschool.work.ui.screen.main.MainIntent import ru.myitschool.work.ui.screen.main.MainIntent
import ru.myitschool.work.ui.screen.main.MainState
import kotlin.text.isEmpty import kotlin.text.isEmpty
class BookViewModel(application: Application) : AndroidViewModel(application) { class BookViewModel(application: Application) : AndroidViewModel(application) {
private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) } private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) }
private val bookingUseCase by lazy { BookingUseCase (BookRepository) }
private val dataStoreManager by lazy { private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager (getApplication() as App).dataStoreManager
} }
@@ -35,6 +41,35 @@ class BookViewModel(application: Application) : AndroidViewModel(application) {
loadBooking() 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() { private fun loadBooking() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_uiState.update { BookState.Loading } _uiState.update { BookState.Loading }
@@ -49,11 +84,27 @@ class BookViewModel(application: Application) : AndroidViewModel(application) {
loadBookingUseCase.invoke(userCode.code).fold( loadBookingUseCase.invoke(userCode.code).fold(
onSuccess = { data -> onSuccess = { data ->
if (data.bookings.isEmpty()) { val availableDates = data.bookings
.filter { it.value.isNotEmpty() }
.keys
.sorted()
if (availableDates.isEmpty()) {
_uiState.update { BookState.Empty } _uiState.update { BookState.Empty }
} 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
)
} }
else {
_uiState.update { BookState.Data(data) }
} }
}, },
onFailure = { error -> onFailure = { error ->
@@ -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
)
}
}
}
} }
} }
} }

View File

@@ -1,9 +1,12 @@
package ru.myitschool.work package ru.myitschool.work
import java.text.SimpleDateFormat
import java.util.Locale
fun String.formatDate(): String { fun String.formatDate(): String {
return try { return try {
val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val outputFormat = java.text.SimpleDateFormat("dd.MM.yyyy", java.util.Locale.getDefault()) val outputFormat = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
val date = inputFormat.parse(this) val date = inputFormat.parse(this)
outputFormat.format(date) outputFormat.format(date)
} catch (e: Exception) { } catch (e: Exception) {
@@ -11,15 +14,13 @@ fun String.formatDate(): String {
} }
} }
fun formatBookingDate(dateString: String): String { fun String.formatBookingDate(): String {
return try { return try {
val parts = dateString.split("-") val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
if (parts.size == 3) { val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault())
"${parts[2]}.${parts[1]}" val date = inputFormat.parse(this)
} else { outputFormat.format(date)
dateString
}
} catch (e: Exception) { } catch (e: Exception) {
dateString this
} }
} }