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

@@ -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.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.layout.Arrangement import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.ButtonColors import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text import androidx.compose.material3.Button
import androidx.compose.runtime.Composable import androidx.compose.material3.ButtonColors
import androidx.compose.runtime.LaunchedEffect import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.collectAsState import androidx.compose.material3.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.runtime.collectAsState
import androidx.compose.ui.draw.clip import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.testTag import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.sp import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.ui.text.style.TextAlign
import androidx.navigation.NavController import androidx.compose.ui.unit.dp
import ru.myitschool.work.R import androidx.compose.ui.unit.sp
import ru.myitschool.work.core.TestIds import androidx.lifecycle.viewmodel.compose.viewModel
import ru.myitschool.work.core.TestIds.Book import androidx.navigation.NavController
import ru.myitschool.work.core.TestIds.Main import ru.myitschool.work.R
import ru.myitschool.work.domain.book.entities.BookingEntity import ru.myitschool.work.core.TestIds
import ru.myitschool.work.formatBookingDate import ru.myitschool.work.core.TestIds.Book
import ru.myitschool.work.formatDate import ru.myitschool.work.core.TestIds.Main
import ru.myitschool.work.ui.BaseButton import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.ui.BaseNoBackgroundButton import ru.myitschool.work.domain.book.entities.PlaceInfo
import ru.myitschool.work.ui.BaseText16 import ru.myitschool.work.formatBookingDate
import ru.myitschool.work.ui.BaseText24 import ru.myitschool.work.formatDate
import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.BaseButton
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.BaseNoBackgroundButton
import ru.myitschool.work.ui.screen.main.MainIntent import ru.myitschool.work.ui.BaseText16
import ru.myitschool.work.ui.theme.Black import ru.myitschool.work.ui.BaseText24
import ru.myitschool.work.ui.theme.Blue import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.theme.Typography import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.theme.White 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 @Composable
fun BookScreen( fun BookScreen(
navController: NavController, navController: NavController,
viewModel: BookViewModel = viewModel(), viewModel: BookViewModel = viewModel(),
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect { action ->
when(action) { when(action) {
is BookAction.Auth -> navController.navigate(AuthScreenDestination) is BookAction.Auth -> navController.navigate(AuthScreenDestination)
is BookAction.Main -> navController.navigate(MainScreenDestination) is BookAction.Main -> navController.navigate(MainScreenDestination)
}
} }
} }
}
when(state) { when(state) {
is BookState.Loading -> { is BookState.Loading -> {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(64.dp) 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 @Composable
fun EmptyContent( fun EmptyContent(
viewModel: BookViewModel 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_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( Box(
modifier = Modifier contentAlignment = Alignment.Center,
.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding(vertical = 20.dp, horizontal = 10.dp)
.clip(RoundedCornerShape(16.dp))
.background(White)
) { ) {
Column( Column(
verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.fillMaxSize() .padding(15.dp)
.padding(13.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( BaseText24(
text = stringResource(R.string.book_choose_place), text = stringResource(R.string.book_all_booked),
style = Typography.bodyMedium, modifier = Modifier.testTag(Book.EMPTY),
fontSize = 16.sp, textAlign = TextAlign.Center
) )
BookPlaceList() Spacer(modifier = Modifier.height(20.dp))
}
BaseButton( BaseButton(
text = stringResource(R.string.booking_button), text = stringResource(R.string.book_back),
btnColor = Blue,
btnContentColor = White,
onClick = { },
modifier = Modifier modifier = Modifier
.testTag(Book.BOOK_BUTTON) .fillMaxWidth()
.padding(horizontal = 10.dp) .testTag(Book.BACK_BUTTON),
.fillMaxWidth(), onClick = { viewModel.onIntent(BookIntent.Back) },
icon = { Image( btnContentColor = White,
painter = painterResource(R.drawable.add_icon), btnColor = Blue
contentDescription = stringResource(R.string.add_icon_description)
) }
) )
} }
} }
} }
}
@Composable @Composable
fun BookPlaceList() { fun ErrorContent(
BookPlaceListElement() viewModel: BookViewModel
}
@Composable
fun BookPlaceListElement() {
}
@Composable
fun BookDateList(dates: List<String>) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier.padding(vertical = 15.dp)
) { ) {
dates.forEach { date -> Box(
BookDateListElement(date = date, onClick = { 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 @Composable
fun BookDateListElement(date: String, onClick: () -> Unit) { fun DataContent(
Button( viewModel: BookViewModel,
contentPadding = PaddingValues(0.dp), bookingData: BookingEntity,
modifier = Modifier selectedDate: String,
.testTag(Book.ITEM_DATE) selectedPlaceId: Int
.padding(0.dp),
border = BorderStroke(1.dp, Black),
onClick = onClick,
colors = ButtonColors(
contentColor = Black,
containerColor = Color.Transparent,
disabledContentColor = Black,
disabledContainerColor = Color.Transparent),
) { ) {
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)
) }
)
}
}
}
} }
}
@Composable
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(
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>,
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,
)
}
}

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 {
else { val selectedDate = availableDates.first()
_uiState.update { BookState.Data(data) } 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 -> onFailure = { error ->
@@ -68,8 +119,8 @@ class BookViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
fun onIntent( intent: BookIntent) { fun onIntent(intent: BookIntent) {
when(intent) { when (intent) {
is BookIntent.LoadBooking -> loadBooking() is BookIntent.LoadBooking -> loadBooking()
is BookIntent.Back -> { 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
)
}
}
}
} }
} }
} }

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