add full logic to BookScreen
Some checks failed
Android Test / validate-and-test (pull_request) Has been cancelled

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> {
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
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<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) {
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<UserEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching {
Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
// Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
// val response = client.get(getUrl(code, Constants.INFO_URL))
// when (response.status) {
// HttpStatusCode.OK -> {
// response.body<UserEntity>()
// }
// else -> error(response.bodyAsText())
// }
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> {
response.body<UserEntity>()
}
else -> error(response.bodyAsText())
}
}
}
suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) {
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))
// when (response.status) {
// HttpStatusCode.OK -> {
// BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
// }
// else -> error(response.bodyAsText())
// }
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> {
BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
}
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 ->
error.printStackTrace()
_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 {
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
}

View File

@@ -3,6 +3,7 @@ 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.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.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
@@ -43,6 +46,7 @@ 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
@@ -86,9 +90,12 @@ fun BookScreen(
}
}
is BookState.Data -> {
val dataState = state as BookState.Data
DataContent(
viewModel,
bookingData = (state as? BookState.Data)?.userBooking
viewModel = viewModel,
bookingData = dataState.userBooking,
selectedDate = dataState.selectedDate,
selectedPlaceId = dataState.selectedPlaceId
)
}
is BookState.Error -> ErrorContent(viewModel)
@@ -190,8 +197,17 @@ fun ErrorContent(
@Composable
fun DataContent(
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 {
Row(
modifier = Modifier
@@ -234,7 +250,13 @@ fun DataContent(
fontSize = 16.sp,
)
BookDateList(bookingData?.bookings?.keys?.toList() ?: emptyList())
BookDateList(
dates = availableDates,
selectedDate = selectedDate,
onDateSelected = { date ->
viewModel.onIntent(BookIntent.SelectDate(date))
}
)
Text(
text = stringResource(R.string.book_choose_place),
@@ -242,14 +264,21 @@ fun DataContent(
fontSize = 16.sp,
)
BookPlaceList()
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 = { },
onClick = { viewModel.onIntent(BookIntent.Book) },
modifier = Modifier
.testTag(Book.BOOK_BUTTON)
.padding(horizontal = 10.dp)
@@ -265,46 +294,131 @@ fun DataContent(
}
@Composable
fun BookPlaceList() {
BookPlaceListElement()
fun BookPlaceList(
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
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
fun BookDateList(dates: List<String>) {
fun BookDateList(
dates: List<String>,
selectedDate: String,
onDateSelected: (String) -> Unit
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier.padding(vertical = 15.dp)
) {
dates.forEach { date ->
BookDateListElement(date = date, onClick = {
// Обработка выбора даты
})
dates.forEachIndexed { index, date ->
BookDateListElement(
date = date,
isSelected = date == selectedDate,
onClick = { onDateSelected(date) },
index = index
)
}
}
}
@Composable
fun BookDateListElement(date: String, onClick: () -> Unit) {
fun BookDateListElement(
date: String,
isSelected: Boolean,
onClick: () -> Unit,
index: Int
) {
Button(
contentPadding = PaddingValues(0.dp),
modifier = Modifier
.testTag(Book.ITEM_DATE)
.testTag(Book.getIdDateItemByPosition(index))
.padding(0.dp),
border = BorderStroke(1.dp, Black),
border = BorderStroke(1.dp, if (isSelected) Blue else Black,),
onClick = onClick,
colors = ButtonColors(
contentColor = Black,
containerColor = Color.Transparent,
contentColor = if (isSelected) White else Black,
containerColor = if (isSelected) Blue else Color.Transparent,
disabledContentColor = Black,
disabledContainerColor = Color.Transparent),
) {
val formattedDate = formatBookingDate(date)
BaseText16(text = formattedDate)
val formattedDate = date.formatBookingDate()
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 {
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
}

View File

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