Compare commits

1 Commits

Author SHA1 Message Date
09e97f9d67 I configured the server connection and improved the logic 2025-12-04 23:19:42 +03:00
12 changed files with 250 additions and 86 deletions

View File

@@ -1,7 +1,7 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { 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 AUTH_URL = "/auth"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" 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 package ru.myitschool.work.data.source
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
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.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.InternalAPI
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.ui.screen.Booking
import ru.myitschool.work.ui.screen.UserInfo
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val client by lazy {
@@ -31,6 +38,40 @@ object NetworkDataSource {
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 {
val response = client.get(getUrl(code, Constants.AUTH_URL)) 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) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> error(response.bodyAsText()) 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable 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.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.book.BookViewModel
@Composable @Composable
fun AppNavHost( fun AppNavHost(
@@ -40,7 +42,10 @@ fun AppNavHost(
} }
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
BookScreen(navController = navController) BookScreen(
viewModel = BookViewModel(),
navController = navController
)
} }
} }
} }

View File

@@ -1,6 +1,10 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.ui.screen.Booking
sealed interface BookIntent { sealed interface BookIntent {
data class Send(val text: String): BookIntent data class Send(val code : String, val booking: Booking) : BookIntent
data class BookingSelect(val text: String): 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; package ru.myitschool.work.ui.screen.book;
import android.util.Log
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.foundation.selection.selectableGroup
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable; import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController; import androidx.navigation.NavController
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import ru.myitschool.work.domain.entities.BookingEntities import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination 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 selectedTime: MutableState<String?> = mutableStateOf(null)
var selectedBooking = mutableStateOf(0) var selectedBooking: MutableState<Booking?> = mutableStateOf(null)
var currentTime = mutableStateOf(0) var currentTime: MutableState<String?> = mutableStateOf(null)
@Composable @Composable
fun BookScreen( fun BookScreen(
viewModel: BookViewModel = viewModel(), viewModel: BookViewModel = viewModel(),
@@ -45,73 +48,72 @@ fun BookScreen(
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { Log.d("BookScreen", "1")
viewModel.navigationFlow.collect {
// TODO настроить наконец это переход между экранами
// Log.d("BookScreen", "2")
when (it) {
BookNavigationEvent.NavigateToMain -> {
Log.d("BookScreen", "3")
navController.navigate(MainScreenDestination) 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( Column(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
// Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value) // Text("" + selectedTime.value + "," + currentTime.value + "," + selectedBooking.value)
when (val currentState = state) { when (state) {
BookState.Data -> { is BookState.Data -> {
TabGroup(options.keys) val options = (state as BookState.Data).booking
if (currentTime.value == null)
var i = 0 for (el in options) {
options.keys.forEach { currentTime.value = el.key
if (i == currentTime.value) break
SelectBooking(options[it]!!)
i ++;
} }
TabGroup(options.keys)
options[currentTime.value]?.let { SelectBooking(it) }
Button( Button(
onClick = { onClick = {
// TODO Добавить бронирование viewModel.onIntent(BookIntent.Send(AuthRepository.getSavedCode()!!, selectedBooking.value!!))
viewModel.onIntent(BookIntent.Send("Данные" ))
}, },
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.BOOK_BUTTON) .testTag(TestIds.Book.BOOK_BUTTON),
enabled = selectedBooking.value != null
) { ) {
Text(stringResource(R.string.to_book)) Text(stringResource(R.string.to_book))
} }
} }
BookState.Error -> { is BookState.Error -> {
Text( Text(
text = "", text = (state as BookState.Error).error,
color = MaterialTheme.colorScheme.error,
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.ERROR) .testTag(TestIds.Book.ERROR)
) )
Button( Button(
onClick = { }, onClick = {
viewModel.onIntent(BookIntent.LoadData)
},
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.REFRESH_BUTTON) .testTag(TestIds.Book.REFRESH_BUTTON)
) { ) {
Text("") Text(stringResource(R.string.upadate)) // А что сюда писать?
} }
} }
@@ -125,7 +127,7 @@ fun BookScreen(
BookState.NotData -> { BookState.NotData -> {
Text( Text(
text = "", text = stringResource(R.string.not_book),
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.EMPTY) .testTag(TestIds.Book.EMPTY)
) )
@@ -133,7 +135,7 @@ fun BookScreen(
} }
Button( Button(
onClick = { onClick = {
navController.navigate(MainScreenDestination) viewModel.onIntent(BookIntent.BackToMainScreen)
}, },
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.BACK_BUTTON) .testTag(TestIds.Book.BACK_BUTTON)
@@ -151,9 +153,9 @@ fun TabGroup(options: Set<String>) {
) { ) {
options.forEachIndexed { index, label -> options.forEachIndexed { index, label ->
NavigationBarItem( NavigationBarItem(
selected = currentTime.value == index, selected = currentTime.value == label,
onClick = { onClick = {
currentTime.value = index currentTime.value = label
}, },
icon = { icon = {
Text( Text(
@@ -170,12 +172,12 @@ fun TabGroup(options: Set<String>) {
} }
@Composable @Composable
fun SelectBooking(options: List<String>) { fun SelectBooking(options: List<Booking>) {
LazyColumn( LazyColumn(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
options.forEachIndexed { index, label -> options.forEachIndexed { index, book ->
item { item {
Row( Row(
Modifier Modifier
@@ -186,14 +188,14 @@ fun SelectBooking(options: List<String>) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = label, text = book.place,
modifier = Modifier modifier = Modifier
.testTag(TestIds.Book.ITEM_PLACE_TEXT) .testTag(TestIds.Book.ITEM_PLACE_TEXT)
) )
RadioButton( RadioButton(
selected = index == selectedBooking.value && currentTime.value == selectedTime.value, selected = book == selectedBooking.value && currentTime.value == selectedTime.value,
onClick = { onClick = {
selectedBooking.value = index selectedBooking.value = book
selectedTime.value = currentTime.value selectedTime.value = currentTime.value
}, },
modifier = Modifier 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 package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.ui.screen.Booking
sealed interface BookState { sealed interface BookState {
object Loading: BookState object Loading: BookState
object Data: BookState data class Data(
object Error: BookState val booking : Map<String, List<Booking>> = mapOf()
): BookState
data class Error(
val error : String
): BookState
object NotData : BookState object NotData : BookState
} }

View File

@@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -10,22 +11,115 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch 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() { class BookViewModel : ViewModel() {
private val _uiState = MutableStateFlow<BookState>(BookState.Data) private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow(); val uiState: StateFlow<BookState> = _uiState.asStateFlow();
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow 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) { fun onIntent(intent: BookIntent) {
when (intent) { when (intent) {
is BookIntent.Send -> { is BookIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
_uiState.update { BookState.Loading } _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="auth_sign_in">Войти</string>
<string name="to_book">забронировать</string> <string name="to_book">забронировать</string>
<string name="back">Назад</string> <string name="back">Назад</string>
<string name="not_book">Всё забранировано</string>
<string name="upadate">Обновить</string>
</resources> </resources>