add booking to main branch #4

Merged
student-18211 merged 3 commits from student-15047/NTO-2025-Android-TeamTask:main into main 2025-12-05 17:24:18 +00:00
12 changed files with 250 additions and 86 deletions
Showing only changes of commit 09e97f9d67 - Show all commits

View File

@@ -1,7 +1,7 @@
package ru.myitschool.work.core
object Constants {
const val HOST = "http://10.0.2.2:8080"
const val HOST = "http://127.0.0.1:8080"
const val AUTH_URL = "/auth"
const val INFO_URL = "/info"
const val BOOKING_URL = "/booking"

View File

@@ -0,0 +1,15 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.ui.screen.Booking
import ru.myitschool.work.ui.screen.UserInfo
object BookRepository {
suspend fun loadBooks(code: String): Result<List<Booking>> {
return NetworkDataSource.getFreeBooking(code)
}
suspend fun sendData(code: String, booking: Booking) : Result<Boolean> {
return NetworkDataSource.createNewBooking(code, booking)
}
}

View File

@@ -1,16 +1,23 @@
package ru.myitschool.work.data.source
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants
import ru.myitschool.work.ui.screen.Booking
import ru.myitschool.work.ui.screen.UserInfo
object NetworkDataSource {
private val client by lazy {
@@ -31,6 +38,40 @@ object NetworkDataSource {
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error("Неверный код для авторизации")
}
}
}
suspend fun getInfo(code: String): Result<UserInfo> = withContext(Dispatchers.IO){
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when(response.status){
HttpStatusCode.OK -> response.body()
else -> error("Ошибка получения данных")
}
}
}
suspend fun getFreeBooking(code: String) : Result<List<Booking>> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> response.body()
else -> error(response.bodyAsText())
}
}
}
@OptIn(InternalAPI::class)
suspend fun createNewBooking(code: String, booking: Booking) : Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
contentType(ContentType.Application.Json)
body = booking
}
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.domain.entities
data class BookingEntities (
var roomName : String,
var time: String
)

View File

@@ -1,7 +0,0 @@
package ru.myitschool.work.domain.entities
data class UserEntities(
val name : String,
var image : Int,
var booking : ArrayList<BookingEntities>
)

View File

@@ -0,0 +1,16 @@
package ru.myitschool.work.ui.screen
import kotlinx.serialization.Serializable
@Serializable
data class UserInfo(
val name: String,
val photo: String? = null,
val bookings: List<Booking>
)
@Serializable
data class Booking(
val date: String,
val place: String
)

View File

@@ -7,6 +7,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -16,6 +17,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.book.BookViewModel
@Composable
fun AppNavHost(
@@ -40,7 +42,10 @@ fun AppNavHost(
}
}
composable<BookScreenDestination> {
BookScreen(navController = navController)
BookScreen(
viewModel = BookViewModel(),
navController = navController
)
}
}
}

View File

@@ -1,6 +1,10 @@
package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.ui.screen.Booking
sealed interface BookIntent {
data class Send(val text: String): BookIntent
data class BookingSelect(val text: String): BookIntent
data class Send(val code : String, val booking: Booking) : BookIntent
data object BackToMainScreen : BookIntent
data object ToAuthScreen : BookIntent
data object LoadData : BookIntent
}

View File

@@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.book;
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -10,12 +11,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable;
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -25,17 +28,17 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController;
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.domain.entities.BookingEntities
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthViewModel
import ru.myitschool.work.ui.screen.Booking
var selectedTime = mutableStateOf(0)
var selectedBooking = mutableStateOf(0)
var currentTime = mutableStateOf(0)
var selectedTime: MutableState<String?> = mutableStateOf(null)
var selectedBooking: MutableState<Booking?> = mutableStateOf(null)
var currentTime: MutableState<String?> = mutableStateOf(null)
@Composable
fun BookScreen(
viewModel: BookViewModel = viewModel(),
@@ -45,73 +48,72 @@ fun BookScreen(
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
Log.d("BookScreen", "1")
viewModel.navigationFlow.collect {
// TODO настроить наконец это переход между экранами
// Log.d("BookScreen", "2")
when (it) {
BookNavigationEvent.NavigateToMain -> {
Log.d("BookScreen", "3")
navController.navigate(MainScreenDestination)
}
BookNavigationEvent.NavigateToAuth -> {
Log.d("BookScreen", "4")
navController.navigate(AuthScreenDestination) {
Log.d("BookScreen", "5")
popUpTo(navController.graph.startDestinationId) {
Log.d("BookScreen", "6")
inclusive = true
}
}
}
}
}
}
// TODO брать данные с сервера
// Иммитация того, что мы взяли данные с сервера
val bookings = arrayListOf(
BookingEntities(
"Рабочее место у окна",
"19.04"
),
BookingEntities(
"Переговорная комната № 1",
"19.04"
),
BookingEntities(
"Коворкинг А",
"19.04"
),
BookingEntities(
"Кабинет № 33",
"20.04"
),
)
val options = toMap(bookings)
Column(
modifier = modifier.fillMaxSize()
) {
// Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value)
when (val currentState = state) {
BookState.Data -> {
when (state) {
is BookState.Data -> {
val options = (state as BookState.Data).booking
if (currentTime.value == null)
for (el in options) {
currentTime.value = el.key
break
}
TabGroup(options.keys)
var i = 0
options.keys.forEach {
if (i == currentTime.value)
SelectBooking(options[it]!!)
i ++;
}
options[currentTime.value]?.let { SelectBooking(it) }
Button(
onClick = {
// TODO Добавить бронирование
viewModel.onIntent(BookIntent.Send("Данные" ))
viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedBooking.value!!))
},
modifier = Modifier
.testTag(TestIds.Book.BOOK_BUTTON)
.testTag(TestIds.Book.BOOK_BUTTON),
enabled = selectedBooking.value != null
) {
Text(stringResource(R.string.to_book))
}
}
BookState.Error -> {
is BookState.Error -> {
Text(
text = "",
text = (state as BookState.Error).error,
color = MaterialTheme.colorScheme.error,
modifier = Modifier
.testTag(TestIds.Book.ERROR)
)
Button(
onClick = { },
onClick = {
viewModel.onIntent(BookIntent.LoadData)
},
modifier = Modifier
.testTag(TestIds.Book.REFRESH_BUTTON)
) {
Text("")
Text(stringResource(R.string.upadate)) // А что сюда писать?
}
}
@@ -125,7 +127,7 @@ fun BookScreen(
BookState.NotData -> {
Text(
text = "",
text = stringResource(R.string.not_book),
modifier = Modifier
.testTag(TestIds.Book.EMPTY)
)
@@ -133,7 +135,7 @@ fun BookScreen(
}
Button(
onClick = {
navController.navigate(MainScreenDestination)
viewModel.onIntent(BookIntent.BackToMainScreen)
},
modifier = Modifier
.testTag(TestIds.Book.BACK_BUTTON)
@@ -151,9 +153,9 @@ fun TabGroup(options: Set<String>) {
) {
options.forEachIndexed { index, label ->
NavigationBarItem(
selected = currentTime.value == index,
selected = currentTime.value == label,
onClick = {
currentTime.value = index
currentTime.value = label
},
icon = {
Text(
@@ -170,12 +172,12 @@ fun TabGroup(options: Set<String>) {
}
@Composable
fun SelectBooking(options: List<String>) {
fun SelectBooking(options: List<Booking>) {
LazyColumn(
Modifier
.fillMaxWidth()
) {
options.forEachIndexed { index, label ->
options.forEachIndexed { index, book ->
item {
Row(
Modifier
@@ -186,14 +188,14 @@ fun SelectBooking(options: List<String>) {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
text = book.place,
modifier = Modifier
.testTag(TestIds.Book.ITEM_PLACE_TEXT)
)
RadioButton(
selected = index == selectedBooking.value && currentTime.value == selectedTime.value,
selected = book == selectedBooking.value && currentTime.value == selectedTime.value,
onClick = {
selectedBooking.value = index
selectedBooking.value = book
selectedTime.value = currentTime.value
},
modifier = Modifier
@@ -205,11 +207,3 @@ fun SelectBooking(options: List<String>) {
}
}
fun toMap(options: List<BookingEntities>) : Map<String, List<String>> {
val map : MutableMap<String, MutableList<String>> = mutableMapOf()
options.forEach {
if (map[it.time] == null) map[it.time] = mutableListOf(it.roomName)
else map[it.time]?.add(it.roomName)
}
return map
}

View File

@@ -1,8 +1,14 @@
package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.ui.screen.Booking
sealed interface BookState {
object Loading: BookState
object Data: BookState
object Error: BookState
data class Data(
val booking : Map<String, List<Booking>> = mapOf()
): BookState
data class Error(
val error : String
): BookState
object NotData : BookState
}

View File

@@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.book
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
@@ -10,22 +11,115 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.BookRepository
import ru.myitschool.work.ui.screen.Booking
class BookViewModel : ViewModel() {
private val _uiState = MutableStateFlow<BookState>(BookState.Data)
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow();
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
private val _navigationFlow = MutableSharedFlow<BookNavigationEvent>()
val navigationFlow: SharedFlow<BookNavigationEvent> = _navigationFlow
init {
loadData()
}
private fun loadData() {
_uiState.update { BookState.Loading }
viewModelScope.launch(Dispatchers.IO) {
val code = AuthRepository.getSavedCode() ?: run {
onIntent(BookIntent.ToAuthScreen)
Log.d("", "Go to AuthScreen")
return@launch
}
Log.d("", "Проверка")
// _uiState.update {
// BookState.Data(
// listOf(
// Booking(
// "19.04",
// "Рабочее место у окна"
// ),
// Booking(
// "19.04",
// "Переговорная комната № 1"
// ),
// Booking(
// "19.04",
// "Коворкинг А"
// ),
// Booking(
// "20.04",
// "Кабинет № 33"
// ),
// ).toMap()
// )
// }
BookRepository.loadBooks(code).fold(
onSuccess = {
it.let { bookings ->
_uiState.update {
when (bookings.isEmpty()) {
true -> BookState.Data(
booking = bookings.toMap()
)
false -> BookState.NotData
}
}
}
},
onFailure = { error ->
_uiState.update {
BookState.Error(
error = error.message ?: "Не удалось загрузить данные"
)
}
}
)
}
}
fun onIntent(intent: BookIntent) {
when (intent) {
is BookIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { BookState.Loading }
BookRepository.sendData(intent.code, intent.booking).fold(
onSuccess = {
_actionFlow.emit(Unit)
},
onFailure = { error ->
BookState.Error(error.message ?: "Неизвестная ошибка")
}
)
}
}
is BookIntent.BookingSelect -> Unit
BookIntent.BackToMainScreen -> {
_navigationFlow.tryEmit(BookNavigationEvent.NavigateToMain)
}
BookIntent.LoadData -> loadData()
BookIntent.ToAuthScreen -> {
_navigationFlow.tryEmit(BookNavigationEvent.NavigateToAuth)
}
}
}
}
fun List<Booking>.toMap() : Map<String, List<Booking>> {
val options = this
val map : MutableMap<String, MutableList<Booking>> = mutableMapOf()
options.forEach {
if (map[it.date] == null) map[it.date] = mutableListOf(it)
else map[it.date]?.add(it)
}
return map
}
sealed interface BookNavigationEvent {
object NavigateToAuth : BookNavigationEvent
object NavigateToMain : BookNavigationEvent
}

View File

@@ -6,4 +6,6 @@
<string name="auth_sign_in">Войти</string>
<string name="to_book">забронировать</string>
<string name="back">Назад</string>
<string name="not_book">Всё забранировано</string>
<string name="upadate">Обновить</string>
</resources>