This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
android:name=".ui.root.RootActivity"
|
android:name=".ui.root.RootActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:label="@string/title_activity_root">
|
android:label="@string/title_activity_root">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookRequestDto(
|
||||||
|
@SerialName("date")
|
||||||
|
val date: String,
|
||||||
|
@SerialName("placeId")
|
||||||
|
val placeId: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ru.myitschool.work.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookingAvailabilityDto(
|
||||||
|
val entries: Map<String, List<BookingOptionDto>>
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookingOptionDto(
|
||||||
|
val id: Int,
|
||||||
|
val place: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.myitschool.work.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserInfoDto(
|
||||||
|
val name: String,
|
||||||
|
@SerialName("photoUrl")
|
||||||
|
val photoUrl: String,
|
||||||
|
val booking: Map<String, UserBookingDto> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserBookingDto(
|
||||||
|
val id: Int,
|
||||||
|
val place: String
|
||||||
|
)
|
||||||
@@ -1,16 +1,48 @@
|
|||||||
package ru.myitschool.work.data.repo
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import ru.myitschool.work.data.storage.AuthLocalDataSource
|
||||||
|
|
||||||
object AuthRepository {
|
object AuthRepository {
|
||||||
|
|
||||||
private var codeCache: String? = null
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
private val _codeState: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||||
|
val codeState: StateFlow<String?> = _codeState.asStateFlow()
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
init {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
scope.launch {
|
||||||
if (success) {
|
AuthLocalDataSource.codeFlow.collect { storedCode ->
|
||||||
codeCache = text
|
_codeState.value = storedCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun checkAndSave(text: String): Result<Unit> {
|
||||||
|
return NetworkDataSource.checkAuth(text).onSuccess {
|
||||||
|
AuthLocalDataSource.saveCode(text)
|
||||||
|
_codeState.value = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentCode(): String? {
|
||||||
|
val cached = _codeState.value
|
||||||
|
if (cached != null) return cached
|
||||||
|
val loaded = AuthLocalDataSource.currentCode()
|
||||||
|
if (loaded != null) {
|
||||||
|
_codeState.value = loaded
|
||||||
|
}
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clear() {
|
||||||
|
AuthLocalDataSource.clear()
|
||||||
|
_codeState.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.model.BookRequestDto
|
||||||
|
import ru.myitschool.work.data.model.BookingOptionDto
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
|
object BookingRepository {
|
||||||
|
|
||||||
|
suspend fun fetchAvailability(code: String): Result<Map<String, List<BookingOptionDto>>> {
|
||||||
|
return NetworkDataSource.fetchBookingAvailability(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createBooking(code: String, date: String, placeId: Int): Result<Unit> {
|
||||||
|
return NetworkDataSource.createBooking(code, BookRequestDto(date = date, placeId = placeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.model.UserInfoDto
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
|
object UserRepository {
|
||||||
|
suspend fun fetchUserInfo(code: String): Result<UserInfoDto> {
|
||||||
|
return NetworkDataSource.fetchUserInfo(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
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.request.setBody
|
||||||
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 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.data.model.BookRequestDto
|
||||||
|
import ru.myitschool.work.data.model.BookingOptionDto
|
||||||
|
import ru.myitschool.work.data.model.UserInfoDto
|
||||||
|
|
||||||
object NetworkDataSource {
|
object NetworkDataSource {
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
@@ -28,11 +36,44 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
suspend fun checkAuth(code: String): Result<Unit> = 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) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> Unit
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchUserInfo(code: String): Result<UserInfoDto> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> response.body()
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchBookingAvailability(code: String): Result<Map<String, List<BookingOptionDto>>> = 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createBooking(code: String, request: BookRequestDto): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.Created -> Unit
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.myitschool.work.data.storage
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
object AuthLocalDataSource {
|
||||||
|
|
||||||
|
private val codeKey = stringPreferencesKey("auth_code")
|
||||||
|
|
||||||
|
val codeFlow: Flow<String?> = SettingsDataStore.dataStore.data
|
||||||
|
.map { preferences -> preferences[codeKey] }
|
||||||
|
|
||||||
|
suspend fun saveCode(code: String) {
|
||||||
|
SettingsDataStore.dataStore.edit { preferences ->
|
||||||
|
preferences[codeKey] = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clear() {
|
||||||
|
SettingsDataStore.dataStore.edit { preferences ->
|
||||||
|
preferences.remove(codeKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun currentCode(): String? {
|
||||||
|
return SettingsDataStore.dataStore.data
|
||||||
|
.map { it[codeKey] }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.myitschool.work.data.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
|
||||||
|
private val Context.authDataStore by preferencesDataStore(name = "auth_settings")
|
||||||
|
|
||||||
|
object SettingsDataStore {
|
||||||
|
val dataStore: DataStore<Preferences> by lazy {
|
||||||
|
App.context.authDataStore
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,7 @@ import ru.myitschool.work.data.repo.AuthRepository
|
|||||||
class CheckAndSaveAuthCodeUseCase(
|
class CheckAndSaveAuthCodeUseCase(
|
||||||
private val repository: AuthRepository
|
private val repository: AuthRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(text: String): Result<Unit> {
|
||||||
text: String
|
return repository.checkAndSave(text)
|
||||||
): Result<Unit> {
|
|
||||||
return repository.checkAndSave(text).mapCatching { success ->
|
|
||||||
if (!success) error("Code is incorrect")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.myitschool.work.ui.nav
|
||||||
|
|
||||||
|
const val BOOKING_RESULT_KEY = "booking_result"
|
||||||
@@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -15,6 +12,8 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination
|
|||||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
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.main.MainScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
@@ -32,18 +31,10 @@ fun AppNavHost(
|
|||||||
AuthScreen(navController = navController)
|
AuthScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable<MainScreenDestination> {
|
composable<MainScreenDestination> {
|
||||||
Box(
|
MainScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
Box(
|
BookScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
sealed interface AuthAction {
|
||||||
|
data object NavigateToMain : AuthAction
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
sealed interface AuthIntent {
|
sealed interface AuthIntent {
|
||||||
data class Send(val text: String): AuthIntent
|
|
||||||
data class TextInput(val text: String): AuthIntent
|
data class TextInput(val text: String): AuthIntent
|
||||||
|
data object Send: AuthIntent
|
||||||
}
|
}
|
||||||
@@ -10,28 +10,30 @@ import androidx.compose.foundation.layout.size
|
|||||||
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
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.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
|
|
||||||
|
private val CODE_REGEX = Regex("^[A-Za-z0-9]{4}$")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthScreen(
|
fun AuthScreen(
|
||||||
viewModel: AuthViewModel = viewModel(),
|
viewModel: AuthViewModel = viewModel(),
|
||||||
@@ -40,11 +42,22 @@ fun AuthScreen(
|
|||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.actionFlow.collect {
|
viewModel.actionFlow.collectLatest {
|
||||||
navController.navigate(MainScreenDestination)
|
when (it) {
|
||||||
|
AuthAction.NavigateToMain -> {
|
||||||
|
navController.navigate(MainScreenDestination) {
|
||||||
|
popUpTo(MainScreenDestination) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.isCheckingSavedCode) {
|
||||||
|
BoxLoading()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -57,41 +70,66 @@ fun AuthScreen(
|
|||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
when (val currentState = state) {
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
is AuthState.Data -> Content(viewModel, currentState)
|
Content(viewModel = viewModel, state = state)
|
||||||
is AuthState.Loading -> {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(64.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
viewModel: AuthViewModel,
|
viewModel: AuthViewModel,
|
||||||
state: AuthState.Data
|
state: AuthUiState
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
val isCodeValidForButton = state.code.length == 4 && CODE_REGEX.matches(state.code)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
|
||||||
TextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
modifier = Modifier
|
||||||
value = inputText,
|
.testTag(TestIds.Auth.CODE_INPUT)
|
||||||
onValueChange = {
|
.fillMaxWidth(),
|
||||||
inputText = it
|
value = state.code,
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
|
||||||
},
|
label = { Text(stringResource(R.string.auth_label)) },
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
placeholder = { Text(stringResource(R.string.auth_label)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = !state.isLoading,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
if (state.showError) {
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auth_error_text),
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
modifier = Modifier
|
||||||
onClick = {
|
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
.fillMaxWidth(),
|
||||||
},
|
onClick = { viewModel.onIntent(AuthIntent.Send) },
|
||||||
enabled = true
|
enabled = !state.isLoading && isCodeValidForButton
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(20.dp))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(R.string.auth_sign_in))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxLoading() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
sealed interface AuthState {
|
data class AuthUiState(
|
||||||
object Loading: AuthState
|
val code: String = "",
|
||||||
object Data: AuthState
|
val isLoading: Boolean = false,
|
||||||
}
|
val showError: Boolean = false,
|
||||||
|
val isCheckingSavedCode: Boolean = true
|
||||||
|
)
|
||||||
@@ -12,32 +12,69 @@ 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.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||||
|
import ru.myitschool.work.ui.screen.auth.AuthIntent.Send
|
||||||
|
import ru.myitschool.work.ui.screen.auth.AuthIntent.TextInput
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel : ViewModel() {
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
private val _uiState = MutableStateFlow(AuthUiState())
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val savedCode = AuthRepository.getCurrentCode()
|
||||||
|
if (savedCode != null) {
|
||||||
|
_actionFlow.emit(AuthAction.NavigateToMain)
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(isCheckingSavedCode = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is TextInput -> onTextChanged(intent.text)
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
Send -> onSendClicked()
|
||||||
_uiState.update { AuthState.Loading }
|
|
||||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
|
||||||
onSuccess = {
|
|
||||||
_actionFlow.emit(Unit)
|
|
||||||
},
|
|
||||||
onFailure = { error ->
|
|
||||||
error.printStackTrace()
|
|
||||||
_actionFlow.emit(Unit)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AuthIntent.TextInput -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onTextChanged(text: String) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
code = text,
|
||||||
|
showError = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSendClicked() {
|
||||||
|
val code = _uiState.value.code
|
||||||
|
if (!isCodeValid(code)) {
|
||||||
|
_uiState.update { it.copy(showError = true) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
|
checkAndSaveAuthCodeUseCase.invoke(code).fold(
|
||||||
|
onSuccess = {
|
||||||
|
_uiState.update { state -> state.copy(isLoading = false, showError = false) }
|
||||||
|
_actionFlow.emit(AuthAction.NavigateToMain)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
_uiState.update { state -> state.copy(isLoading = false, showError = true) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCodeValid(code: String): Boolean {
|
||||||
|
if (code.length != 4) return false
|
||||||
|
if (code.isBlank()) return false
|
||||||
|
val regex = "^[A-Za-z0-9]{4}$".toRegex()
|
||||||
|
return regex.matches(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
|
import ru.myitschool.work.ui.nav.BOOKING_RESULT_KEY
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: BookingViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.actionFlow.collectLatest { action ->
|
||||||
|
when (action) {
|
||||||
|
BookingAction.NavigateToAuth -> {
|
||||||
|
navController.navigate(AuthScreenDestination) {
|
||||||
|
popUpTo(AuthScreenDestination) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookingAction.CloseWithSuccess -> {
|
||||||
|
navController.previousBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.set(BOOKING_RESULT_KEY, true)
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
state.isLoading -> BookLoading()
|
||||||
|
state.showError -> BookError(
|
||||||
|
onRefresh = { viewModel.onIntent(BookingIntent.Refresh) },
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
state.showEmpty -> BookEmpty(onBack = { navController.popBackStack() })
|
||||||
|
else -> BookContent(
|
||||||
|
state = state,
|
||||||
|
onSelectDate = { viewModel.onIntent(BookingIntent.SelectDate(it)) },
|
||||||
|
onSelectPlace = { viewModel.onIntent(BookingIntent.SelectPlace(it)) },
|
||||||
|
onBook = { viewModel.onIntent(BookingIntent.Book) },
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookLoading() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookError(onRefresh: () -> Unit, onBack: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ERROR),
|
||||||
|
text = stringResource(R.string.book_error_text),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Book.REFRESH_BUTTON),
|
||||||
|
onClick = onRefresh
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.book_refresh))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Book.BACK_BUTTON),
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.book_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookEmpty(onBack: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.EMPTY),
|
||||||
|
text = stringResource(R.string.book_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Book.BACK_BUTTON),
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.book_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookContent(
|
||||||
|
state: BookingUiState,
|
||||||
|
onSelectDate: (Int) -> Unit,
|
||||||
|
onSelectPlace: (Int) -> Unit,
|
||||||
|
onBook: () -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
ScrollableTabRow(selectedTabIndex = state.selectedDateIndex) {
|
||||||
|
state.dates.forEachIndexed { index, date ->
|
||||||
|
Tab(
|
||||||
|
selected = state.selectedDateIndex == index,
|
||||||
|
onClick = { onSelectDate(index) }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
.testTag(TestIds.Book.getIdDateItemByPosition(index)),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE),
|
||||||
|
text = date.displayDate,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val places = state.dates.getOrNull(state.selectedDateIndex)?.places.orEmpty()
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(places) { index, place ->
|
||||||
|
PlaceItem(
|
||||||
|
index = index,
|
||||||
|
title = place.title,
|
||||||
|
selected = index == state.selectedPlaceIndex,
|
||||||
|
onClick = { onSelectPlace(index) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TestIds.Book.BACK_BUTTON),
|
||||||
|
onClick = onBack
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.book_back))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TestIds.Book.BOOK_BUTTON),
|
||||||
|
enabled = state.canBook,
|
||||||
|
onClick = onBook
|
||||||
|
) {
|
||||||
|
if (state.isBooking) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.height(16.dp))
|
||||||
|
} else {
|
||||||
|
Text(text = stringResource(R.string.book_book))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlaceItem(
|
||||||
|
index: Int,
|
||||||
|
title: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = if (selected) 4.dp else 0.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
|
||||||
|
.selectable(
|
||||||
|
selected = selected,
|
||||||
|
role = Role.RadioButton,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR),
|
||||||
|
selected = selected,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT),
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookingAction {
|
||||||
|
data object CloseWithSuccess : BookingAction
|
||||||
|
data object NavigateToAuth : BookingAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookingIntent {
|
||||||
|
data object Refresh : BookingIntent
|
||||||
|
data class SelectDate(val index: Int) : BookingIntent
|
||||||
|
data class SelectPlace(val index: Int) : BookingIntent
|
||||||
|
data object Book : BookingIntent
|
||||||
|
data object Back : BookingIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
data class BookingUiState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val showError: Boolean = false,
|
||||||
|
val showEmpty: Boolean = false,
|
||||||
|
val dates: ImmutableList<BookingDateUi> = persistentListOf(),
|
||||||
|
val selectedDateIndex: Int = 0,
|
||||||
|
val selectedPlaceIndex: Int = -1,
|
||||||
|
val isBooking: Boolean = false
|
||||||
|
) {
|
||||||
|
val canBook: Boolean
|
||||||
|
get() = dates.isNotEmpty() &&
|
||||||
|
selectedDateIndex in dates.indices &&
|
||||||
|
selectedPlaceIndex in dates[selectedDateIndex].places.indices &&
|
||||||
|
!isBooking
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BookingDateUi(
|
||||||
|
val isoDate: String,
|
||||||
|
val displayDate: String,
|
||||||
|
val places: ImmutableList<BookingPlaceUi>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingPlaceUi(
|
||||||
|
val id: Int,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
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.BookingRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class BookingViewModel : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(BookingUiState())
|
||||||
|
val uiState: StateFlow<BookingUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actionFlow: MutableSharedFlow<BookingAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<BookingAction> = _actionFlow
|
||||||
|
|
||||||
|
private val isoFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
private val displayFormatter = DateTimeFormatter.ofPattern("dd.MM")
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIntent(intent: BookingIntent) {
|
||||||
|
when (intent) {
|
||||||
|
BookingIntent.Refresh -> refresh()
|
||||||
|
is BookingIntent.SelectDate -> selectDate(intent.index)
|
||||||
|
is BookingIntent.SelectPlace -> selectPlace(intent.index)
|
||||||
|
BookingIntent.Book -> book()
|
||||||
|
BookingIntent.Back -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = true,
|
||||||
|
showError = false,
|
||||||
|
showEmpty = false,
|
||||||
|
isBooking = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val code = AuthRepository.getCurrentCode()
|
||||||
|
if (code == null) {
|
||||||
|
_actionFlow.emit(BookingAction.NavigateToAuth)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
BookingRepository.fetchAvailability(code).fold(
|
||||||
|
onSuccess = { map ->
|
||||||
|
val dates = map.entries
|
||||||
|
.filter { it.value.isNotEmpty() }
|
||||||
|
.sortedBy { LocalDate.parse(it.key, isoFormatter) }
|
||||||
|
.map { entry ->
|
||||||
|
BookingDateUi(
|
||||||
|
isoDate = entry.key,
|
||||||
|
displayDate = LocalDate.parse(entry.key, isoFormatter).format(displayFormatter),
|
||||||
|
places = entry.value.map { option ->
|
||||||
|
BookingPlaceUi(
|
||||||
|
id = option.id,
|
||||||
|
title = option.place
|
||||||
|
)
|
||||||
|
}.toImmutableList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
|
||||||
|
val showEmpty = dates.isEmpty()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
showError = false,
|
||||||
|
showEmpty = showEmpty,
|
||||||
|
dates = dates,
|
||||||
|
selectedDateIndex = 0,
|
||||||
|
selectedPlaceIndex = if (!showEmpty && dates[0].places.isNotEmpty()) 0 else -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(isLoading = false, showError = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectDate(index: Int) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
if (index !in state.dates.indices) state
|
||||||
|
else state.copy(
|
||||||
|
selectedDateIndex = index,
|
||||||
|
selectedPlaceIndex = if (state.dates[index].places.isNotEmpty()) 0 else -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectPlace(index: Int) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
val currentPlaces = state.dates.getOrNull(state.selectedDateIndex)?.places
|
||||||
|
?: return@update state
|
||||||
|
if (index !in currentPlaces.indices) return@update state
|
||||||
|
state.copy(selectedPlaceIndex = index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun book() {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
if (!currentState.canBook) return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val code = AuthRepository.getCurrentCode()
|
||||||
|
if (code == null) {
|
||||||
|
_actionFlow.emit(BookingAction.NavigateToAuth)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val date = currentState.dates[currentState.selectedDateIndex]
|
||||||
|
val place = date.places[currentState.selectedPlaceIndex]
|
||||||
|
_uiState.update { it.copy(isBooking = true) }
|
||||||
|
BookingRepository.createBooking(code, date.isoDate, place.id).fold(
|
||||||
|
onSuccess = {
|
||||||
|
_uiState.update { it.copy(isBooking = false) }
|
||||||
|
_actionFlow.emit(BookingAction.CloseWithSuccess)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
_uiState.update { it.copy(isBooking = false, showError = true) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
data object NavigateToAuth : MainAction
|
||||||
|
data object NavigateToBooking : MainAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainIntent {
|
||||||
|
data object Refresh : MainIntent
|
||||||
|
data object Logout : MainIntent
|
||||||
|
data object AddBooking : MainIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
|
import ru.myitschool.work.ui.nav.BOOKING_RESULT_KEY
|
||||||
|
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainAction.NavigateToAuth
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainAction.NavigateToBooking
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileAvatar(
|
||||||
|
name: String,
|
||||||
|
photoUrl: String?
|
||||||
|
) {
|
||||||
|
val modifier = Modifier
|
||||||
|
.size(72.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.testTag(TestIds.Main.PROFILE_IMAGE)
|
||||||
|
if (photoUrl.isNullOrBlank()) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val placeholder = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
|
||||||
|
Text(
|
||||||
|
text = placeholder,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = modifier,
|
||||||
|
model = photoUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: MainViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.actionFlow.collectLatest { action ->
|
||||||
|
when (action) {
|
||||||
|
NavigateToAuth -> {
|
||||||
|
navController.navigate(AuthScreenDestination) {
|
||||||
|
popUpTo(AuthScreenDestination) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigateToBooking -> {
|
||||||
|
navController.navigate(BookScreenDestination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val refreshFlow = navController.currentBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.getStateFlow(BOOKING_RESULT_KEY, false)
|
||||||
|
|
||||||
|
LaunchedEffect(refreshFlow) {
|
||||||
|
refreshFlow?.collectLatest { shouldRefresh ->
|
||||||
|
if (shouldRefresh) {
|
||||||
|
viewModel.onIntent(MainIntent.Refresh)
|
||||||
|
navController.currentBackStackEntry?.savedStateHandle?.set(BOOKING_RESULT_KEY, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
state.isLoading -> MainLoading()
|
||||||
|
state.showError -> MainError(onRetry = { viewModel.onIntent(MainIntent.Refresh) })
|
||||||
|
else -> MainContent(
|
||||||
|
state = state,
|
||||||
|
onRefresh = { viewModel.onIntent(MainIntent.Refresh) },
|
||||||
|
onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) },
|
||||||
|
onLogout = { viewModel.onIntent(MainIntent.Logout) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainLoading() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainError(onRetry: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||||
|
text = stringResource(R.string.main_error_text),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||||
|
onClick = onRetry
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_refresh))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainContent(
|
||||||
|
state: MainUiState,
|
||||||
|
onRefresh: () -> Unit,
|
||||||
|
onAddBooking: () -> Unit,
|
||||||
|
onLogout: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
ProfileAvatar(
|
||||||
|
name = state.name,
|
||||||
|
photoUrl = state.photoUrl
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME),
|
||||||
|
text = state.name,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TestIds.Main.ADD_BUTTON),
|
||||||
|
onClick = onAddBooking
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_add))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||||
|
onClick = onRefresh
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_refresh))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||||
|
onClick = onLogout
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.main_logout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_bookings_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.bookings.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_empty_list),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(state.bookings) { index, booking ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestIds.Main.getIdItemByPosition(index)),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
|
||||||
|
text = booking.dateDisplay,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
|
||||||
|
text = booking.place,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
data class MainUiState(
|
||||||
|
val isLoading: Boolean = true,
|
||||||
|
val showError: Boolean = false,
|
||||||
|
val name: String = "",
|
||||||
|
val photoUrl: String? = null,
|
||||||
|
val bookings: ImmutableList<MainBookingUi> = persistentListOf()
|
||||||
|
) {
|
||||||
|
val hasContent: Boolean get() = !isLoading && !showError
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MainBookingUi(
|
||||||
|
val id: Int,
|
||||||
|
val dateIso: String,
|
||||||
|
val dateDisplay: String,
|
||||||
|
val place: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.core.Constants
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainAction.NavigateToAuth
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainAction.NavigateToBooking
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class MainViewModel : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(MainUiState())
|
||||||
|
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||||
|
|
||||||
|
private val dateFormatterServer = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
private val dateFormatterDisplay = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIntent(intent: MainIntent) {
|
||||||
|
when (intent) {
|
||||||
|
MainIntent.Refresh -> refresh()
|
||||||
|
MainIntent.Logout -> logout()
|
||||||
|
MainIntent.AddBooking -> openBooking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { it.copy(isLoading = true, showError = false) }
|
||||||
|
val code = AuthRepository.getCurrentCode()
|
||||||
|
if (code == null) {
|
||||||
|
_actionFlow.emit(NavigateToAuth)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
UserRepository.fetchUserInfo(code).fold(
|
||||||
|
onSuccess = { info ->
|
||||||
|
val bookings = info.booking.entries
|
||||||
|
.sortedBy { LocalDate.parse(it.key, dateFormatterServer) }
|
||||||
|
.mapIndexed { index, entry ->
|
||||||
|
val date = LocalDate.parse(entry.key, dateFormatterServer)
|
||||||
|
MainBookingUi(
|
||||||
|
id = index,
|
||||||
|
dateIso = entry.key,
|
||||||
|
dateDisplay = date.format(dateFormatterDisplay),
|
||||||
|
place = entry.value.place
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList()
|
||||||
|
val resolvedPhoto = info.photoUrl.let { url ->
|
||||||
|
if (url.isBlank()) null
|
||||||
|
else if (url.startsWith("http")) url
|
||||||
|
else "${Constants.HOST}$url"
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
showError = false,
|
||||||
|
name = info.name,
|
||||||
|
photoUrl = resolvedPhoto,
|
||||||
|
bookings = bookings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(isLoading = false, showError = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBooking() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_actionFlow.emit(NavigateToBooking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logout() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
AuthRepository.clear()
|
||||||
|
_actionFlow.emit(NavigateToAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,19 @@
|
|||||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
<string name="auth_title">Привет! Введи код для авторизации</string>
|
||||||
<string name="auth_label">Код</string>
|
<string name="auth_label">Код</string>
|
||||||
<string name="auth_sign_in">Войти</string>
|
<string name="auth_sign_in">Войти</string>
|
||||||
|
<string name="auth_error_text">Неверный код или произошла ошибка. Попробуйте снова.</string>
|
||||||
|
<string name="main_logout">Выйти</string>
|
||||||
|
<string name="main_refresh">Обновить</string>
|
||||||
|
<string name="main_add">Забронировать</string>
|
||||||
|
<string name="main_error_text">Не удалось загрузить данные. Попробуйте обновить.</string>
|
||||||
|
<string name="main_bookings_title">Ваши бронирования</string>
|
||||||
|
<string name="main_empty_list">Нет активных бронирований</string>
|
||||||
|
<string name="book_title">Бронирование</string>
|
||||||
|
<string name="book_book">Забронировать</string>
|
||||||
|
<string name="book_back">Назад</string>
|
||||||
|
<string name="book_error_text">Не удалось получить данные. Повторите попытку.</string>
|
||||||
|
<string name="book_refresh">Обновить</string>
|
||||||
|
<string name="book_select_date">Выберите дату</string>
|
||||||
|
<string name="book_select_place">Выберите место</string>
|
||||||
|
<string name="book_empty">Всё забронировано</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user