From be88863cb2a01d939122ce795e1c23ab2e7f5a24 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 23 Nov 2025 15:52:39 +0300 Subject: [PATCH 01/16] Basic screens --- .../work/ui/screen/NavigationGraph.kt | 14 ++++---------- .../work/ui/screen/auth/AuthScreen.kt | 6 ++++-- .../work/ui/screen/book/BookScreen.kt | 16 ++++++++++++++++ .../work/ui/screen/main/MainScreen.kt | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 01b0f32..7890fab 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -15,6 +15,8 @@ import ru.myitschool.work.ui.nav.AuthScreenDestination 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.main.MainScreen @Composable fun AppNavHost( @@ -32,18 +34,10 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + MainScreen(navController = navController) } composable { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") - } + BookScreen(navController = navController) } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index f99978e..a8aac84 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -91,7 +91,9 @@ private fun Content( viewModel.onIntent(AuthIntent.Send(inputText)) }, enabled = true - ) { - Text(stringResource(R.string.auth_sign_in)) + + ) { Text(stringResource(R.string.auth_sign_in)) + + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt new file mode 100644 index 0000000..b225888 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,16 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.navigation.NavController + +@Composable +fun BookScreen(navController: NavController){ + Box( + contentAlignment = Alignment.Center + ) { + Text(text = "Hello") + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt new file mode 100644 index 0000000..c9d1101 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,17 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.navigation.NavController + +@Composable +fun MainScreen(navController: NavController) { + Box( + contentAlignment = Alignment.Center + ) { + Text(text = "Hello") + } +} + -- 2.34.1 From bcbca3a10fb3dc606f5a6ab864638ce17ad4864d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 24 Nov 2025 18:33:16 +0300 Subject: [PATCH 02/16] Auth first steps --- .../ru/myitschool/work/core/OurConstants.kt | 9 +++++++ .../work/data/repo/AuthRepository.kt | 6 +++-- .../work/data/source/DataStoreDataSource.kt | 27 +++++++++++++++++++ .../work/data/source/NetworkDataSource.kt | 2 +- .../work/ui/screen/auth/AuthAction.kt | 6 +++++ .../work/ui/screen/auth/AuthScreen.kt | 16 ++++++++--- .../work/ui/screen/auth/AuthViewModel.kt | 14 +++++----- 7 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/core/OurConstants.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt diff --git a/app/src/main/java/ru/myitschool/work/core/OurConstants.kt b/app/src/main/java/ru/myitschool/work/core/OurConstants.kt new file mode 100644 index 0000000..ea99bec --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/OurConstants.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.core + +import androidx.datastore.preferences.core.intPreferencesKey + +// Не добавляйте ничего, что уже есть в Constants! +object OurConstants { + const val SHABLON = "^[a-zA-Z0-9]*\$" + const val DS_AUTH_KEY = "authkey" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..febfcbb 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,15 +1,17 @@ package ru.myitschool.work.data.repo +import android.content.Context +import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { - private var codeCache: String? = null - + // TODO: разобраться с контекстом suspend fun checkAndSave(text: String): Result { return NetworkDataSource.checkAuth(text).onSuccess { success -> if (success) { codeCache = text + createAuthCode(context = appContext, code = text) } } } diff --git a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt new file mode 100644 index 0000000..16bd7ee --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -0,0 +1,27 @@ +package ru.myitschool.work.data.source + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import ru.myitschool.work.core.OurConstants.DS_AUTH_KEY + +val Context.dataStore: DataStore by preferencesDataStore(name = "auth") +val AUTH_KEY = stringPreferencesKey(DS_AUTH_KEY) + +object DataStoreDataSource { + fun authFlow(context : Context): Flow = context.dataStore.data.map { preferences -> + (preferences[AUTH_KEY] ?: 0).toString() + } + // TODO: разобраться с контекстом + suspend fun createAuthCode (context : Context, code : String) { + context.dataStore.updateData { + it.toMutablePreferences().also { preferences -> + preferences[AUTH_KEY] = code + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index fbdfef5..f12b719 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -30,7 +30,7 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) + val response = client.get(getUrl(code, Constants.AUTH_URL)) // TODO: Отпрвка запроса на сервер when (response.status) { HttpStatusCode.OK -> true else -> error(response.bodyAsText()) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt new file mode 100644 index 0000000..939a280 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.auth + +sealed interface AuthAction { + data class ShowError(val message: String) : AuthAction + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index a8aac84..845e8ab 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import ru.myitschool.work.R +import ru.myitschool.work.core.OurConstants.SHABLON import ru.myitschool.work.core.TestIds import ru.myitschool.work.ui.nav.MainScreenDestination @@ -74,6 +75,7 @@ private fun Content( state: AuthState.Data ) { var inputText by remember { mutableStateOf("") } + var errorText : String? by remember { mutableStateOf(null) } Spacer(modifier = Modifier.size(16.dp)) TextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), @@ -88,12 +90,20 @@ private fun Content( Button( modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) + if (!inputText.isEmpty() || inputText.length == 4 || inputText.matches(Regex(SHABLON))){ + viewModel.onIntent(AuthIntent.Send(inputText)) + } }, enabled = true - ) { Text(stringResource(R.string.auth_sign_in)) + ) { Text(stringResource(R.string.auth_sign_in)) } + ShowError(errorText) // TODO: раскидать в коде когда показывается ошибка и когда нет +} +@Composable +fun ShowError(text : String?){ + if (text != null){ + Text(text, modifier = Modifier.testTag(TestIds.Auth.ERROR)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 3153640..4f5ea07 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -18,21 +18,23 @@ class AuthViewModel : ViewModel() { private val _uiState = MutableStateFlow(AuthState.Data) val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() - val actionFlow: SharedFlow = _actionFlow + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: AuthIntent) { when (intent) { is AuthIntent.Send -> { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { _uiState.update { AuthState.Loading } - checkAndSaveAuthCodeUseCase.invoke("9999").fold( + checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { - _actionFlow.emit(Unit) + // TODO: Поведение при успехе }, onFailure = { error -> error.printStackTrace() - _actionFlow.emit(Unit) + if (error.message != null) { + _actionFlow.emit(AuthAction.ShowError(error.message.toString())) + } } ) } -- 2.34.1 From 474b282fbbb6b792c2ee83f233e1c647b584bb99 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 27 Nov 2025 09:12:16 +0300 Subject: [PATCH 03/16] auth base --- .../work/data/repo/AuthRepository.kt | 9 ++++++--- .../work/data/source/DataStoreDataSource.kt | 18 +++++++++++++----- .../ru/myitschool/work/ui/root/RootActivity.kt | 3 +++ .../work/ui/screen/NavigationGraph.kt | 2 ++ .../work/ui/screen/auth/AuthIntent.kt | 1 + .../work/ui/screen/auth/AuthScreen.kt | 7 +++++-- .../work/ui/screen/auth/AuthState.kt | 1 + .../work/ui/screen/auth/AuthViewModel.kt | 18 +++++++++++++++++- 8 files changed, 48 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index febfcbb..555399d 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -6,13 +6,16 @@ import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { private var codeCache: String? = null - // TODO: разобраться с контекстом suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> + /* return NetworkDataSource.checkAuth(text).onSuccess { success -> if (success) { codeCache = text - createAuthCode(context = appContext, code = text) + createAuthCode(code = text) } } + } */ + codeCache = text + createAuthCode(code = text) + return Result.success(true) // TODO: ВЕРНУТЬ СЕТЕВОЙ ЗАПРОС } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt index 16bd7ee..aaa03ce 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -1,26 +1,34 @@ package ru.myitschool.work.data.source import android.content.Context +import android.util.Log +import androidx.compose.material3.rememberTimePickerState import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import ru.myitschool.work.App import ru.myitschool.work.core.OurConstants.DS_AUTH_KEY + val Context.dataStore: DataStore by preferencesDataStore(name = "auth") val AUTH_KEY = stringPreferencesKey(DS_AUTH_KEY) object DataStoreDataSource { - fun authFlow(context : Context): Flow = context.dataStore.data.map { preferences -> - (preferences[AUTH_KEY] ?: 0).toString() + fun authFlow(): Flow { + Log.d("AnnaKonda", "Code is checking") + return App.context.dataStore.data.map { preferences -> + (preferences[AUTH_KEY] ?: 0).toString() + } } - // TODO: разобраться с контекстом - suspend fun createAuthCode (context : Context, code : String) { - context.dataStore.updateData { + + suspend fun createAuthCode(code: String) { + App.context.dataStore.updateData { it.toMutablePreferences().also { preferences -> preferences[AUTH_KEY] = code + Log.d("AnnaKonda", "Code added to ds") } } } diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt index 54b156d..2efc340 100644 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt @@ -8,12 +8,15 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier +import ru.myitschool.work.App +import ru.myitschool.work.data.source.DataStoreDataSource.authFlow import ru.myitschool.work.ui.screen.AppNavHost import ru.myitschool.work.ui.theme.WorkTheme class RootActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + App.context = applicationContext enableEdgeToEdge() setContent { WorkTheme { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 7890fab..e969861 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -11,10 +11,12 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import ru.myitschool.work.data.source.DataStoreDataSource.authFlow import ru.myitschool.work.ui.nav.AuthScreenDestination 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.auth.AuthViewModel import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.main.MainScreen diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt index 74f200a..632f50d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthIntent { data class Send(val text: String): AuthIntent data class TextInput(val text: String): AuthIntent + object CheckLogIntent: AuthIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 845e8ab..98fcc85 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -39,13 +39,13 @@ fun AuthScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() + viewModel.onIntent(AuthIntent.CheckLogIntent) LaunchedEffect(Unit) { viewModel.actionFlow.collect { navController.navigate(MainScreenDestination) } } - Column( modifier = Modifier .fillMaxSize() @@ -65,6 +65,9 @@ fun AuthScreen( modifier = Modifier.size(64.dp) ) } + is AuthState.LoggedIn -> { + navController.navigate(MainScreenDestination) + } } } } @@ -90,7 +93,7 @@ private fun Content( Button( modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), onClick = { - if (!inputText.isEmpty() || inputText.length == 4 || inputText.matches(Regex(SHABLON))){ + if (!inputText.isEmpty() && inputText.length == 4 && inputText.matches(Regex(SHABLON))){ viewModel.onIntent(AuthIntent.Send(inputText)) } }, diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt index a06ba76..7af8dda 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt @@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthState { object Loading: AuthState object Data: AuthState + object LoggedIn: AuthState } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 4f5ea07..c99dda7 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.auth +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -11,6 +12,7 @@ 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.source.DataStoreDataSource.authFlow import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase class AuthViewModel : ViewModel() { @@ -28,7 +30,7 @@ class AuthViewModel : ViewModel() { _uiState.update { AuthState.Loading } checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( onSuccess = { - // TODO: Поведение при успехе + _uiState.update { AuthState.LoggedIn } }, onFailure = { error -> error.printStackTrace() @@ -40,6 +42,20 @@ class AuthViewModel : ViewModel() { } } is AuthIntent.TextInput -> Unit + is AuthIntent.CheckLogIntent -> { + viewModelScope.launch { + _uiState.update { AuthState.Loading } + authFlow().collect { + Log.d("AnnaKonda", it) + if (it != "0"){ + _uiState.update { AuthState.LoggedIn } + } else { + _uiState.update { AuthState.Data } + } + } + } + } } } + } \ No newline at end of file -- 2.34.1 From ad113e0438be784781c1a369110a380f418c3b48 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 27 Nov 2025 10:10:25 +0300 Subject: [PATCH 04/16] error shows --- .../work/ui/screen/auth/AuthAction.kt | 3 +- .../work/ui/screen/auth/AuthScreen.kt | 50 +++++++++++++------ .../work/ui/screen/auth/AuthViewModel.kt | 10 +++- app/src/main/res/values/strings.xml | 2 + 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt index 939a280..b2171cf 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthAction { - data class ShowError(val message: String) : AuthAction + data class ShowError(val message: String?) : AuthAction + object LogIn : AuthAction } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 98fcc85..767a4f6 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.auth +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -28,6 +29,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import io.ktor.util.collections.setValue +import ru.myitschool.work.App import ru.myitschool.work.R import ru.myitschool.work.core.OurConstants.SHABLON import ru.myitschool.work.core.TestIds @@ -39,13 +42,18 @@ fun AuthScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() - viewModel.onIntent(AuthIntent.CheckLogIntent) - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { + viewModel.onIntent(AuthIntent.CheckLogIntent) + } + + val event = viewModel.actionFlow.collectAsState(initial = null) + + LaunchedEffect(event.value) { + if (event.value is AuthAction.LogIn) { navController.navigate(MainScreenDestination) } } + Log.d("AnnaKonda", state.javaClass.toString()) Column( modifier = Modifier .fillMaxSize() @@ -65,6 +73,7 @@ fun AuthScreen( modifier = Modifier.size(64.dp) ) } + is AuthState.LoggedIn -> { navController.navigate(MainScreenDestination) } @@ -78,10 +87,21 @@ private fun Content( state: AuthState.Data ) { var inputText by remember { mutableStateOf("") } - var errorText : String? by remember { mutableStateOf(null) } + var errorText: String? by remember { mutableStateOf(null) } + + val event = viewModel.actionFlow.collectAsState(initial = null) + + LaunchedEffect(event.value) { + if (event.value is AuthAction.ShowError) { + errorText = (event.value as AuthAction.ShowError).message + } + } + Spacer(modifier = Modifier.size(16.dp)) TextField( - modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), + modifier = Modifier + .testTag(TestIds.Auth.CODE_INPUT) + .fillMaxWidth(), value = inputText, onValueChange = { inputText = it @@ -91,22 +111,20 @@ private fun Content( ) Spacer(modifier = Modifier.size(16.dp)) Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), + modifier = Modifier + .testTag(TestIds.Auth.SIGN_BUTTON) + .fillMaxWidth(), onClick = { - if (!inputText.isEmpty() && inputText.length == 4 && inputText.matches(Regex(SHABLON))){ + if (!inputText.isEmpty() && inputText.length == 4 && inputText.matches(Regex(SHABLON))) { viewModel.onIntent(AuthIntent.Send(inputText)) + } else { + errorText = App.context.getString(R.string.auth_nasty_code) } }, enabled = true ) { Text(stringResource(R.string.auth_sign_in)) } - ShowError(errorText) // TODO: раскидать в коде когда показывается ошибка и когда нет - -} - -@Composable -fun ShowError(text : String?){ - if (text != null){ - Text(text, modifier = Modifier.testTag(TestIds.Auth.ERROR)) + if (errorText != null) { + Text(errorText.toString(), modifier = Modifier.testTag(TestIds.Auth.ERROR)) } -} +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index c99dda7..1e516ac 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.screen.auth import android.util.Log +import androidx.compose.ui.res.stringResource import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -11,6 +12,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ru.myitschool.work.App +import ru.myitschool.work.R import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.source.DataStoreDataSource.authFlow import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase @@ -37,19 +40,23 @@ class AuthViewModel : ViewModel() { if (error.message != null) { _actionFlow.emit(AuthAction.ShowError(error.message.toString())) } + _uiState.update { AuthState.Data } } ) } } + is AuthIntent.TextInput -> Unit is AuthIntent.CheckLogIntent -> { viewModelScope.launch { _uiState.update { AuthState.Loading } authFlow().collect { Log.d("AnnaKonda", it) - if (it != "0"){ + if (it != "0") { + _actionFlow.emit(AuthAction.LogIn) _uiState.update { AuthState.LoggedIn } } else { + _actionFlow.emit(AuthAction.ShowError(App.context.getString(R.string.auth_wrong_code))) _uiState.update { AuthState.Data } } } @@ -57,5 +64,4 @@ class AuthViewModel : ViewModel() { } } } - } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa8bda6..44675a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,6 @@ Привет! Введи код для авторизации Код Войти + Введён неверный код + Неправильный формат кода \ No newline at end of file -- 2.34.1 From 8fa01383d4a87ba01ad15f496b3588b73fd2d2d6 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 28 Nov 2025 08:27:06 +0300 Subject: [PATCH 05/16] auth without server done --- .../main/java/ru/myitschool/work/core/Utils.kt | 11 +++++++++++ .../myitschool/work/ui/screen/auth/AuthAction.kt | 4 ++-- .../myitschool/work/ui/screen/auth/AuthScreen.kt | 13 ++++++++++--- .../work/ui/screen/auth/AuthViewModel.kt | 15 +++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/core/Utils.kt diff --git a/app/src/main/java/ru/myitschool/work/core/Utils.kt b/app/src/main/java/ru/myitschool/work/core/Utils.kt new file mode 100644 index 0000000..1505df2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/Utils.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.core + +import ru.myitschool.work.core.OurConstants.SHABLON + +class Utils { + companion object { + fun CheckCodeInput(text : String) : Boolean{ + return !text.isEmpty() && text.length == 4 && text.matches(Regex(SHABLON)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt index b2171cf..fbab1c2 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt @@ -2,6 +2,6 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthAction { data class ShowError(val message: String?) : AuthAction - object LogIn : AuthAction - + data class LogIn(val isLogged: Boolean): AuthAction + data class AuthBtnEnabled(val enabled: Boolean) : AuthAction } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 767a4f6..30b60a5 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -34,6 +34,7 @@ import ru.myitschool.work.App import ru.myitschool.work.R import ru.myitschool.work.core.OurConstants.SHABLON import ru.myitschool.work.core.TestIds +import ru.myitschool.work.core.Utils import ru.myitschool.work.ui.nav.MainScreenDestination @Composable @@ -50,7 +51,9 @@ fun AuthScreen( LaunchedEffect(event.value) { if (event.value is AuthAction.LogIn) { - navController.navigate(MainScreenDestination) + if ((event.value as AuthAction.LogIn).isLogged) { + navController.navigate(MainScreenDestination) + } } } Log.d("AnnaKonda", state.javaClass.toString()) @@ -88,12 +91,16 @@ private fun Content( ) { var inputText by remember { mutableStateOf("") } var errorText: String? by remember { mutableStateOf(null) } + var btnEnabled: Boolean by remember { mutableStateOf(false) } val event = viewModel.actionFlow.collectAsState(initial = null) LaunchedEffect(event.value) { if (event.value is AuthAction.ShowError) { errorText = (event.value as AuthAction.ShowError).message + } else if (event.value is AuthAction.AuthBtnEnabled){ + Log.d("AnnaKonda", btnEnabled.toString()) + btnEnabled = if ((event.value as AuthAction.AuthBtnEnabled).enabled){ true } else { false } } } @@ -115,13 +122,13 @@ private fun Content( .testTag(TestIds.Auth.SIGN_BUTTON) .fillMaxWidth(), onClick = { - if (!inputText.isEmpty() && inputText.length == 4 && inputText.matches(Regex(SHABLON))) { + if (Utils.CheckCodeInput(inputText)) { viewModel.onIntent(AuthIntent.Send(inputText)) } else { errorText = App.context.getString(R.string.auth_nasty_code) } }, - enabled = true + enabled = btnEnabled ) { Text(stringResource(R.string.auth_sign_in)) } if (errorText != null) { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 1e516ac..4900be0 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.App import ru.myitschool.work.R +import ru.myitschool.work.core.Utils.Companion.CheckCodeInput import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.source.DataStoreDataSource.authFlow import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase @@ -46,14 +47,24 @@ class AuthViewModel : ViewModel() { } } - is AuthIntent.TextInput -> Unit + is AuthIntent.TextInput -> { + viewModelScope.launch { + authFlow().collect { + if (CheckCodeInput(intent.text)) { + _actionFlow.emit(AuthAction.AuthBtnEnabled(true)) + } else { + _actionFlow.emit(AuthAction.AuthBtnEnabled(false)) + } + } + } + } is AuthIntent.CheckLogIntent -> { viewModelScope.launch { _uiState.update { AuthState.Loading } authFlow().collect { Log.d("AnnaKonda", it) if (it != "0") { - _actionFlow.emit(AuthAction.LogIn) + _actionFlow.emit(AuthAction.LogIn(true)) _uiState.update { AuthState.LoggedIn } } else { _actionFlow.emit(AuthAction.ShowError(App.context.getString(R.string.auth_wrong_code))) -- 2.34.1 From 7f889337d96d3555acb9be4666b923afe28117e9 Mon Sep 17 00:00:00 2001 From: Dell Date: Sat, 29 Nov 2025 21:31:31 +0300 Subject: [PATCH 06/16] our commit message --- .../work/ui/screen/NavigationGraph.kt | 18 +- .../work/ui/screen/auth/AuthScreen.kt | 1 - .../work/ui/screen/book/BookScreen.kt | 166 +++++++++++++- .../work/ui/screen/book/BookingViewModel.kt | 87 ++++++++ .../work/ui/screen/main/MainScreen.kt | 208 +++++++++++++++++- 5 files changed, 458 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 7890fab..5577ca7 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -34,10 +31,21 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - MainScreen(navController = navController) + MainScreen( + navController = navController, + onNavigateToBooking = { + navController.navigate(BookScreenDestination) + } + ) } composable { - BookScreen(navController = navController) + BookScreen( + onBack = { navController.popBackStack() }, + onBookingSuccess = { + // Возвращаемся на главный экран и обновляем его + navController.popBackStack() + } + ) } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index a8aac84..c9f4d5f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index b225888..cdbcfb4 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -1,16 +1,168 @@ package ru.myitschool.work.ui.screen.book -import androidx.compose.foundation.layout.Box import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.navigation.NavController +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.selection.selectable +import androidx.compose.material3.Button +import androidx.compose.material3.RadioButton +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.viewmodel.compose.viewModel @Composable -fun BookScreen(navController: NavController){ - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") +fun BookingScreen( + uiState: BookingUiState, // состояние интерфейса + onSelectDate: (LocalDate) -> Unit, // callback при выборе даты + onSelectPlace: (String) -> Unit, // callback при выборе места + onBook: () -> Unit, // callback при бронировании + onBack: () -> Unit, // callback при нажатии "Назад" + onRefresh: () -> Unit // callback при обновлении +) { + // Сортировка дат по порядку + val sortedDates = uiState.dates.sorted() + // Фильтрация дат, для которых есть доступные места + val availableDates = sortedDates.filter { date -> uiState.places[date]?.isNotEmpty() == true } + + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + // Вкладки для выбора дат + if (availableDates.isNotEmpty()) { + ScrollableTabRow(selectedTabIndex = availableDates.indexOf(uiState.selectedDate)) { + availableDates.forEachIndexed { index, date -> + Tab( + selected = date == uiState.selectedDate, + onClick = { onSelectDate(date) }, + text = { + Text( + text = date.format(DateTimeFormatter.ofPattern("dd.MM")), + modifier = Modifier.testTag("book_date_pos_$index") + ) + }, + modifier = Modifier.testTag("book_date") + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Список мест для выбранной даты + val placesForDate = uiState.selectedDate?.let { uiState.places[it] } ?: emptyList() + + if (placesForDate.isNotEmpty()) { + Column { + placesForDate.forEachIndexed { index, place -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .selectable( + selected = uiState.selectedPlace == place, + onClick = { onSelectPlace(place) } + ) + .testTag("book_place_pos_$index"), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = place, + modifier = Modifier.weight(1f).testTag("book_place_text") + ) + RadioButton( + selected = uiState.selectedPlace == place, + onClick = { onSelectPlace(place) }, + modifier = Modifier.testTag("book_place_selector") + ) + } + } + } + } + + // пустой список (все забронировано) + if (availableDates.isEmpty() && !uiState.isError) { + Text( + text = "Всё забронировано", + modifier = Modifier.testTag("book_empty") + ) + } + + // ошибка + if (uiState.isError) { + Text( + text = uiState.errorMessage ?: "Ошибка загрузки", + color = Color.Red, + modifier = Modifier.testTag("book_error") + ) + + Button( + onClick = onRefresh, + modifier = Modifier.testTag("book_refresh_button") + ) { + Text("Обновить") + } + } + + Spacer(modifier = Modifier.weight(1f)) + + // Кнопки: Забронировать и Назад + if (!uiState.isError && placesForDate.isNotEmpty()) { + Button( + onClick = onBook, + enabled = uiState.selectedPlace != null, // активна только при выбранном месте + modifier = Modifier.fillMaxWidth().testTag("book_book_button") + ) { Text("Забронировать") } + } + + Button( + onClick = onBack, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag("book_back_button") + ) { + Text("Назад") + } } +} + +// Модель состояния интерфейса +data class BookingUiState( + val dates: List = emptyList(), // список доступных дат + val places: Map> = emptyMap(), // места по датам + val selectedDate: LocalDate? = null, // выбранная дата + val selectedPlace: String? = null, // выбранное место + val isError: Boolean = false, // флаг ошибки + val errorMessage: String? = null // сообщение об ошибке +) + +@Composable +fun BookScreen( + onBack: () -> Unit, // callback при возврате назад + onBookingSuccess: () -> Unit // callback при успешном бронировании +) { + val viewModel: BookingViewModel = viewModel() + val uiState by viewModel.uiState.collectAsState() + + BookingScreen( + uiState = uiState, + onSelectDate = { date -> viewModel.selectDate(date) }, + onSelectPlace = { place -> viewModel.selectPlace(place) }, + onBook = { + viewModel.bookPlace() + onBookingSuccess() + }, + onBack = onBack, + onRefresh = { viewModel.refresh() } + ) } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt new file mode 100644 index 0000000..928017d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt @@ -0,0 +1,87 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.time.LocalDate + +class BookingViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(BookingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadBookingData() + } + + fun loadBookingData() { + viewModelScope.launch { + try { + // Временные mock данные + val mockDates = listOf( + LocalDate.now().plusDays(1), + LocalDate.now().plusDays(2), + LocalDate.now().plusDays(3) + ) + + val mockPlaces = mapOf( + mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), + mockDates[1] to listOf("Место 1", "Место 2"), + mockDates[2] to listOf("Место 1") + ) + + val sortedDates = mockDates.sorted() + val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } + val defaultDate = availableDates.firstOrNull() + + _uiState.value = _uiState.value.copy( + dates = sortedDates, + places = mockPlaces, + selectedDate = defaultDate, + selectedPlace = null, + isError = false, + errorMessage = null + ) + + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isError = true, + errorMessage = "Ошибка загрузки данных" + ) + } + } + } + + fun selectDate(date: LocalDate) { + _uiState.value = _uiState.value.copy( + selectedDate = date, + selectedPlace = null + ) + } + + fun selectPlace(place: String) { + _uiState.value = _uiState.value.copy( + selectedPlace = place + ) + } + + fun bookPlace() { + viewModelScope.launch { + try { + //вызов API для бронирования + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isError = true, + errorMessage = "Ошибка бронирования" + ) + } + } + } + + fun refresh() { + loadBookingData() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index c9d1101..f931280 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -1,17 +1,207 @@ package ru.myitschool.work.ui.screen.main -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* +import ru.myitschool.work.ui.nav.BookScreenDestination + +// Модель данных для бронирования +data class BookingItem( + val date: String, // Формат "dd.MM.yyyy" + val place: String, + val id: Int +) @Composable -fun MainScreen(navController: NavController) { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "Hello") +fun MainScreen( + navController: NavController, + onNavigateToBooking: () -> Unit +) { + // Состояния + var userName by remember { mutableStateOf("Иван Иванов") } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var bookingItems by remember { mutableStateOf(emptyList()) } + var hasError by remember { mutableStateOf(false) } + + // Для корутин + val coroutineScope = rememberCoroutineScope() + + // Функция загрузки данных + fun loadData() { + isLoading = true + hasError = false + + coroutineScope.launch { + kotlinx.coroutines.delay(1000) // Имитация задержки + + // Имитация ответа от сервера + val response = listOf( + BookingItem("20.12.2023", "Конференц-зал А", 1), + BookingItem("15.12.2023", "Переговорная Б", 2), + BookingItem("25.12.2023", "Спортзал", 3) + ) + + // Сортировка по дате (увеличение) + bookingItems = response.sortedBy { + SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).parse(it.date) + } + + isLoading = false + } + } + + // Первая загрузка при открытии экрана + LaunchedEffect(Unit) { + loadData() + } + + // Если ошибка - показываем только ошибку и кнопку обновления + if (hasError) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Текстовое поле с ошибкой (main_error) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопка обновления (main_refresh_button) + Button(onClick = { loadData() }) { + Text("Обновить") + } + } + } else { + // Нормальное состояние + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // Верхняя строка + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Фото пользователя (main_photo) + Image( + painter = painterResource(id = android.R.drawable.ic_menu_gallery), + contentDescription = "Фото", + modifier = Modifier.size(64.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + // Имя пользователя (main_name) + Text( + text = userName, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + + // Кнопка выхода (main_logout_button) + Button(onClick = { + // Очистка данных и переход на авторизацию + userName = "" + bookingItems = emptyList() + navController.navigate("auth") { popUpTo(0) } + }) { + Text("Выход") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопки действий + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Кнопка обновления (main_refresh_button) + Button( + onClick = { loadData() }, + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Обновить") + } + } + + Button( + onClick = { navController.navigate(BookScreenDestination) } + ) { + Text("Перейти к бронированию") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Список бронирований + if (bookingItems.isNotEmpty()) { + LazyColumn(modifier = Modifier.weight(1f)) { + items(bookingItems) { item -> + // Элемент списка (main_book_pos_{index}) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + // Дата бронирования (main_item_date) + Text( + text = "Дата: ${item.date}", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Место бронирования (main_item_place) + Text( + text = "Место: ${item.place}", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } else { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Нет бронирований", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + } } } - -- 2.34.1 From e1797b4c1690f27c08542a5e9052b79ef4f74855 Mon Sep 17 00:00:00 2001 From: Dell Date: Sun, 30 Nov 2025 15:31:17 +0300 Subject: [PATCH 07/16] =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B8=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=20=D0=B1=D1=80=D0=BE=D0=BD=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../work/ui/screen/main/MainScreen.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index f931280..a851c25 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -1,17 +1,21 @@ package ru.myitschool.work.ui.screen.main +import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import kotlinx.coroutines.launch +import ru.myitschool.work.core.TestIds import java.text.SimpleDateFormat import java.util.* import ru.myitschool.work.ui.nav.BookScreenDestination @@ -77,7 +81,9 @@ fun MainScreen( // Текстовое поле с ошибкой (main_error) Text( text = errorMessage, - color = MaterialTheme.colorScheme.error + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + ) Spacer(modifier = Modifier.height(16.dp)) @@ -103,7 +109,7 @@ fun MainScreen( Image( painter = painterResource(id = android.R.drawable.ic_menu_gallery), contentDescription = "Фото", - modifier = Modifier.size(64.dp) + modifier = Modifier.size(64.dp).testTag(TestIds.Main.PROFILE_IMAGE) ) Spacer(modifier = Modifier.width(16.dp)) @@ -112,7 +118,7 @@ fun MainScreen( Text( text = userName, style = MaterialTheme.typography.titleLarge, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME), color = MaterialTheme.colorScheme.onSurface ) @@ -122,9 +128,12 @@ fun MainScreen( userName = "" bookingItems = emptyList() navController.navigate("auth") { popUpTo(0) } - }) { + }, + modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) + ) { Text("Выход") } + } Spacer(modifier = Modifier.height(16.dp)) @@ -137,7 +146,8 @@ fun MainScreen( // Кнопка обновления (main_refresh_button) Button( onClick = { loadData() }, - enabled = !isLoading + enabled = !isLoading, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) ) { if (isLoading) { CircularProgressIndicator( @@ -148,9 +158,11 @@ fun MainScreen( Text("Обновить") } } - + // кнопка бронирования Button( - onClick = { navController.navigate(BookScreenDestination) } + onClick = { navController.navigate(BookScreenDestination) + }, + modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) ) { Text("Перейти к бронированию") } @@ -160,9 +172,13 @@ fun MainScreen( // Список бронирований if (bookingItems.isNotEmpty()) { - LazyColumn(modifier = Modifier.weight(1f)) { - items(bookingItems) { item -> + LazyColumn( + modifier = Modifier.weight(1f) + + ) { + itemsIndexed(bookingItems) { index, item -> // Элемент списка (main_book_pos_{index}) + Log.d("Nicoly", index.toString()) Card( modifier = Modifier .fillMaxWidth() @@ -171,11 +187,12 @@ fun MainScreen( containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(16.dp).testTag(TestIds.Main.getIdItemByPosition(index))) { // Дата бронирования (main_item_date) Text( text = "Дата: ${item.date}", - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) ) Spacer(modifier = Modifier.height(4.dp)) @@ -183,7 +200,8 @@ fun MainScreen( // Место бронирования (main_item_place) Text( text = "Место: ${item.place}", - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) ) } } -- 2.34.1 From 257755a25acec834dad3b8f28e405e032f2fedc7 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 2 Dec 2025 21:08:22 +0300 Subject: [PATCH 08/16] full mainscreen --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../ru/myitschool/work/data/entity/Booking.kt | 11 + .../myitschool/work/data/entity/Employee.kt | 9 + .../ru/myitschool/work/data/entity/Place.kt | 5 + .../work/data/repo/MainRepository.kt | 34 +++ .../work/data/source/DataStoreDataSource.kt | 16 +- .../work/data/source/NetworkDataSource.kt | 68 ++++++ .../work/domain/main/GetUserDataUseCase.kt | 13 ++ .../work/ui/screen/NavigationGraph.kt | 7 +- .../work/ui/screen/auth/AuthViewModel.kt | 2 +- .../work/ui/screen/book/BookScreen.kt | 15 +- .../work/ui/screen/book/BookingState.kt | 12 ++ .../work/ui/screen/book/BookingViewModel.kt | 4 +- .../work/ui/screen/main/MainAction.kt | 8 + .../work/ui/screen/main/MainIntent.kt | 9 + .../work/ui/screen/main/MainScreen.kt | 194 ++++++++++-------- .../work/ui/screen/main/MainState.kt | 9 + .../work/ui/screen/main/MainViewModel.kt | 66 ++++++ 18 files changed, 375 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/entity/Booking.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/entity/Employee.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/entity/Place.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/main/GetUserDataUseCase.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index a8b7cc5..5fe3adf 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://10.0.2.2:8080" + const val HOST = "http://192.168.1.39:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/entity/Booking.kt b/app/src/main/java/ru/myitschool/work/data/entity/Booking.kt new file mode 100644 index 0000000..e3fe124 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/entity/Booking.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.data.entity + +import java.time.LocalDate + + +data class Booking ( val id: Long, + val date: LocalDate, + val place: Place, + val employeeCode: String){ + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/entity/Employee.kt b/app/src/main/java/ru/myitschool/work/data/entity/Employee.kt new file mode 100644 index 0000000..8190ff4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/entity/Employee.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.entity + +data class Employee ( + val name: String, + val code: String, + val photoUrl: String, + val bookingList: MutableList) { + +} diff --git a/app/src/main/java/ru/myitschool/work/data/entity/Place.kt b/app/src/main/java/ru/myitschool/work/data/entity/Place.kt new file mode 100644 index 0000000..73e5a73 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/entity/Place.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.data.entity + +data class Place( + val id: Long, + val place: String ){} diff --git a/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt new file mode 100644 index 0000000..003b689 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,34 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.entity.Employee +import ru.myitschool.work.data.source.DataStoreDataSource +import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode +import ru.myitschool.work.data.source.DataStoreDataSource.getAuthCode +import ru.myitschool.work.data.source.NetworkDataSource + +class MainRepository { + private var employee: Employee? = null + + suspend fun getUserInfo(): Result { + return try { + val code = getCode() + val result = NetworkDataSource.getUserInfo(code) + result.onSuccess { success -> + employee = success + } + result + } catch (e: Exception) { + Result.failure(e) + } + } + + + + suspend fun getCode(): String { + return getAuthCode() + } + + suspend fun logOut(){ + DataStoreDataSource.logOut() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt index aaa03ce..5014e2f 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import ru.myitschool.work.App import ru.myitschool.work.core.OurConstants.DS_AUTH_KEY @@ -28,7 +29,20 @@ object DataStoreDataSource { App.context.dataStore.updateData { it.toMutablePreferences().also { preferences -> preferences[AUTH_KEY] = code - Log.d("AnnaKonda", "Code added to ds") + } + } + } + + suspend fun getAuthCode(): String { + return App.context.dataStore.data.map { preferences -> + preferences[AUTH_KEY] ?: "" + }.first() + } + + suspend fun logOut() { + App.context.dataStore.updateData { + it.toMutablePreferences().also { preferences -> + preferences.remove(AUTH_KEY) } } } diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index f12b719..546f664 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -11,6 +11,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.data.entity.Employee +import kotlinx.serialization.json.* +import ru.myitschool.work.data.entity.Booking +import ru.myitschool.work.data.entity.Place +import java.time.LocalDate object NetworkDataSource { private val client by lazy { @@ -37,6 +42,69 @@ object NetworkDataSource { } } } + suspend fun getUserInfo(code: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when (response.status) { + HttpStatusCode.OK -> { + val json = response.bodyAsText() + if (json.isBlank()) { + error("Пустой ответ от сервера") + } + + val jsonObject = try { + Json.parseToJsonElement(json).jsonObject + } catch (e: Exception) { + error("Ошибка парсинга: ${e.message}") + } + val name = jsonObject["name"]?.jsonPrimitive?.content + ?: error("Отсутствует поле 'name'") + val photoUrl = jsonObject["photoUrl"]?.jsonPrimitive?.content + ?: error("Отсутствует поле 'photoUrl'") + + val bookingJson = jsonObject["booking"]?.jsonObject + ?: error("Отсутствует поле 'booking' в ответе") + + val employee = Employee( + name = name, + code = code, + photoUrl = photoUrl, + bookingList = mutableListOf() + ) + val bookingList = mutableListOf() + for ((dateString, bookingElement) in bookingJson) { + val date = LocalDate.parse(dateString) + val bookingObj = bookingElement.jsonObject + val bookingId = bookingObj["id"]?.jsonPrimitive?.long + ?: error("Отсутствует поле id") + val placeString = bookingObj["place"]?.jsonPrimitive?.content + ?: error("Отсутствует поле 'place' $dateString") + + if (placeString.isBlank()) { + error("Пустое поле 'place' $dateString") + } + + val placeId = bookingId + val place = Place(placeId, placeString) + + val booking = Booking( + id = bookingId, + date = date, + place = place, + employeeCode = employee.code + ) + bookingList.add(booking) + } + if (bookingList.isEmpty()) { + error("Список бронирований пуст") + } + employee.bookingList.addAll(bookingList) + employee + } + else -> error(response.bodyAsText()) + } + } + } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/GetUserDataUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/main/GetUserDataUseCase.kt new file mode 100644 index 0000000..9b8c749 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/GetUserDataUseCase.kt @@ -0,0 +1,13 @@ +package ru.myitschool.work.domain.main + +import ru.myitschool.work.data.entity.Employee +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.repo.MainRepository + +class GetUserDataUseCase( + private val repository: MainRepository +) { + suspend operator fun invoke(): Result { + return repository.getUserInfo() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index bc632cd..93e99c6 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -33,12 +33,7 @@ fun AppNavHost( AuthScreen(navController = navController) } composable { - MainScreen( - navController = navController, - onNavigateToBooking = { - navController.navigate(BookScreenDestination) - } - ) + MainScreen(navController = navController) } composable { BookScreen( diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 4900be0..0a0ac5b 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -47,7 +47,7 @@ class AuthViewModel : ViewModel() { } } - is AuthIntent.TextInput -> { + is AuthIntent.TextInput -> { viewModelScope.launch { authFlow().collect { if (CheckCodeInput(intent.text)) { diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index cdbcfb4..0953e8b 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -27,7 +27,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun BookingScreen( - uiState: BookingUiState, // состояние интерфейса + uiState: BookingState, // состояние интерфейса onSelectDate: (LocalDate) -> Unit, // callback при выборе даты onSelectPlace: (String) -> Unit, // callback при выборе места onBook: () -> Unit, // callback при бронировании @@ -136,22 +136,15 @@ fun BookingScreen( } } -// Модель состояния интерфейса -data class BookingUiState( - val dates: List = emptyList(), // список доступных дат - val places: Map> = emptyMap(), // места по датам - val selectedDate: LocalDate? = null, // выбранная дата - val selectedPlace: String? = null, // выбранное место - val isError: Boolean = false, // флаг ошибки - val errorMessage: String? = null // сообщение об ошибке -) + + @Composable fun BookScreen( onBack: () -> Unit, // callback при возврате назад onBookingSuccess: () -> Unit // callback при успешном бронировании ) { - val viewModel: BookingViewModel = viewModel() + val viewModel: BookingViewModel = BookingViewModel() val uiState by viewModel.uiState.collectAsState() BookingScreen( diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt new file mode 100644 index 0000000..b61dcc7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.ui.screen.book + +import java.time.LocalDate + +data class BookingState( + val dates: List = emptyList(), // список доступных дат + val places: Map> = emptyMap(), // места по датам + val selectedDate: LocalDate? = null, // выбранная дата + val selectedPlace: String? = null, // выбранное место + val isError: Boolean = false, // флаг ошибки + val errorMessage: String? = null // сообщение об ошибке +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt index 928017d..96133c8 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt @@ -10,8 +10,8 @@ import java.time.LocalDate class BookingViewModel : ViewModel() { - private val _uiState = MutableStateFlow(BookingUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(BookingState()) + val uiState: StateFlow = _uiState.asStateFlow() init { loadBookingData() diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt new file mode 100644 index 0000000..007981d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainAction.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.ui.screen.main + +import ru.myitschool.work.ui.screen.auth.AuthAction + +sealed interface MainAction { + data class SetName(val name: String) + data class ShowError(val message: String?) : MainAction +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt new file mode 100644 index 0000000..eb58973 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.ui.screen.main + +sealed interface MainIntent { + /* data class Send(val text: String): AuthIntent + data class TextInput(val text: String): AuthIntent + object CheckLogIntent: AuthIntent*/ + object LoadData: MainIntent + object LogOut: MainIntent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index a851c25..5460355 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -1,100 +1,81 @@ package ru.myitschool.work.ui.screen.main import android.util.Log -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import kotlinx.coroutines.launch +import coil3.compose.AsyncImage import ru.myitschool.work.core.TestIds -import java.text.SimpleDateFormat -import java.util.* +import ru.myitschool.work.data.entity.Booking +import ru.myitschool.work.data.entity.Employee +import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination -// Модель данных для бронирования -data class BookingItem( - val date: String, // Формат "dd.MM.yyyy" - val place: String, - val id: Int -) - @Composable fun MainScreen( navController: NavController, - onNavigateToBooking: () -> Unit ) { + val viewModel = MainViewModel() // Состояния - var userName by remember { mutableStateOf("Иван Иванов") } - var isLoading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf("") } - var bookingItems by remember { mutableStateOf(emptyList()) } - var hasError by remember { mutableStateOf(false) } - - // Для корутин - val coroutineScope = rememberCoroutineScope() - + val event = viewModel.actionFlow.collectAsState(initial = null) // Функция загрузки данных - fun loadData() { - isLoading = true - hasError = false - - coroutineScope.launch { - kotlinx.coroutines.delay(1000) // Имитация задержки - - // Имитация ответа от сервера - val response = listOf( - BookingItem("20.12.2023", "Конференц-зал А", 1), - BookingItem("15.12.2023", "Переговорная Б", 2), - BookingItem("25.12.2023", "Спортзал", 3) - ) - - // Сортировка по дате (увеличение) - bookingItems = response.sortedBy { - SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).parse(it.date) - } - - isLoading = false - } - } - - // Первая загрузка при открытии экрана LaunchedEffect(Unit) { - loadData() + viewModel.onIntent(MainIntent.LoadData) } + var errorMessage: String? by remember { mutableStateOf("") } + LaunchedEffect(event.value) { + if (event.value is MainAction.ShowError) { + errorMessage = (event.value as MainAction.ShowError).message + } + } + Log.d("AnnaKonda", errorMessage.toString()) // Если ошибка - показываем только ошибку и кнопку обновления - if (hasError) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - // Текстовое поле с ошибкой (main_error) - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.testTag(TestIds.Main.ERROR) + if (errorMessage != null) { + ErrorScreen(viewModel = viewModel, navController = navController, errorMessage) + } else { + DefaultScreen(viewModel = viewModel, navController = navController) + } +} +@Composable +fun DefaultScreen(viewModel: MainViewModel, + navController: NavController){ + val state by viewModel.uiState.collectAsState() + var employee : Employee? by remember { mutableStateOf(null) } + var errorMessage by remember { mutableStateOf("") } + var bookingItems : List? by remember { mutableStateOf(emptyList()) } + var isLoading by remember { mutableStateOf(true) } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Кнопка обновления (main_refresh_button) - Button(onClick = { loadData() }) { - Text("Обновить") + LaunchedEffect(state) { + when (state) { + is MainState.Loading -> { + errorMessage = "" + isLoading = true + } + is MainState.Data -> { + isLoading = false + employee = (state as MainState.Data).employee + if (employee == null){ + navController.navigate(AuthScreenDestination) { popUpTo(0) } + } else { + bookingItems = employee?.bookingList?.sortedBy { item -> + item?.date + } + } } } - } else { - // Нормальное состояние + } + employee?.let { Column( modifier = Modifier .fillMaxSize() @@ -106,29 +87,34 @@ fun MainScreen( verticalAlignment = Alignment.CenterVertically ) { // Фото пользователя (main_photo) - Image( - painter = painterResource(id = android.R.drawable.ic_menu_gallery), + employee?.photoUrl?.let { msg -> Log.d("AnnaKonda", msg) } + AsyncImage( + model = employee?.photoUrl ?: "", contentDescription = "Фото", - modifier = Modifier.size(64.dp).testTag(TestIds.Main.PROFILE_IMAGE) + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE), + error = painterResource(id = android.R.drawable.ic_menu_gallery) ) Spacer(modifier = Modifier.width(16.dp)) // Имя пользователя (main_name) Text( - text = userName, + text = employee!!.name, style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME), color = MaterialTheme.colorScheme.onSurface ) // Кнопка выхода (main_logout_button) - Button(onClick = { - // Очистка данных и переход на авторизацию - userName = "" - bookingItems = emptyList() - navController.navigate("auth") { popUpTo(0) } - }, + Button( + onClick = { + // Очистка данных и переход на авторизацию + viewModel.onIntent(MainIntent.LogOut) + bookingItems = emptyList() + }, modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) ) { Text("Выход") @@ -145,11 +131,11 @@ fun MainScreen( ) { // Кнопка обновления (main_refresh_button) Button( - onClick = { loadData() }, - enabled = !isLoading, + onClick = { viewModel.onIntent(MainIntent.LoadData) }, + enabled = state !is MainState.Loading, modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) ) { - if (isLoading) { + if (state is MainState.Loading) { CircularProgressIndicator( modifier = Modifier.size(16.dp), color = MaterialTheme.colorScheme.onPrimary @@ -160,7 +146,8 @@ fun MainScreen( } // кнопка бронирования Button( - onClick = { navController.navigate(BookScreenDestination) + onClick = { + navController.navigate(BookScreenDestination) }, modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) ) { @@ -171,14 +158,13 @@ fun MainScreen( Spacer(modifier = Modifier.height(16.dp)) // Список бронирований - if (bookingItems.isNotEmpty()) { + if (!bookingItems.isNullOrEmpty()) { LazyColumn( modifier = Modifier.weight(1f) ) { - itemsIndexed(bookingItems) { index, item -> + itemsIndexed(bookingItems as List) { index, item -> // Элемент списка (main_book_pos_{index}) - Log.d("Nicoly", index.toString()) Card( modifier = Modifier .fillMaxWidth() @@ -187,10 +173,13 @@ fun MainScreen( containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { - Column(modifier = Modifier.padding(16.dp).testTag(TestIds.Main.getIdItemByPosition(index))) { + Column( + modifier = Modifier.padding(16.dp) + .testTag(TestIds.Main.getIdItemByPosition(index)) + ) { // Дата бронирования (main_item_date) Text( - text = "Дата: ${item.date}", + text = "Дата: ${item?.date}", color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) ) @@ -199,7 +188,7 @@ fun MainScreen( // Место бронирования (main_item_place) Text( - text = "Место: ${item.place}", + text = "Место: ${item?.place?.place}", color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) ) @@ -223,3 +212,34 @@ fun MainScreen( } } } + + + + +@Composable +fun ErrorScreen(viewModel: MainViewModel, + navController: NavController, + errorMessage: String?){ + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Текстовое поле с ошибкой (main_error) + + if (errorMessage != null) { + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.testTag(TestIds.Main.ERROR) + + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { viewModel.onIntent(MainIntent.LoadData) }) { + Text("Обновить") + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt new file mode 100644 index 0000000..ea877f5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.ui.screen.main + +import ru.myitschool.work.data.entity.Employee +import ru.myitschool.work.ui.screen.auth.AuthState + +sealed interface MainState { + object Loading: MainState + data class Data (val employee: Employee?): MainState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt new file mode 100644 index 0000000..0b3b065 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,66 @@ +package ru.myitschool.work.ui.screen.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.MainRepository +import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase +import ru.myitschool.work.domain.main.GetUserDataUseCase +import ru.myitschool.work.ui.screen.auth.AuthAction +import ru.myitschool.work.ui.screen.auth.AuthIntent +import ru.myitschool.work.ui.screen.auth.AuthState + +class MainViewModel : ViewModel() { + init { + loadData() + } + private val repository by lazy{ MainRepository() } + private val getUserDataUseCase by lazy { GetUserDataUseCase(repository) } + + private val _uiState = MutableStateFlow(MainState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + fun onIntent(intent: MainIntent) { + when (intent) { + is MainIntent.LoadData -> { + loadData() + } + is MainIntent.LogOut -> { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { MainState.Data(null) } + repository.logOut() + } + } + } + } + + fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { MainState.Loading } + + getUserDataUseCase.invoke().fold( + onSuccess = { employee -> + _uiState.update { MainState.Data(employee) } + _actionFlow.emit(MainAction.ShowError(null)) + }, + onFailure = { error -> + error.printStackTrace() + if (error.message != null) { + _actionFlow.emit(MainAction.ShowError(error.message.toString())) + } + } + ) + } + } +} \ No newline at end of file -- 2.34.1 From bc0b73dfd2b1289691f01c3daeb7e72d55e0a48d Mon Sep 17 00:00:00 2001 From: Dell Date: Wed, 3 Dec 2025 16:08:02 +0300 Subject: [PATCH 09/16] =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../work/ui/screen/book/BookScreen.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index cdbcfb4..c64acee 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -24,6 +24,7 @@ import java.time.format.DateTimeFormatter import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel +import ru.myitschool.work.core.TestIds @Composable fun BookingScreen( @@ -42,7 +43,9 @@ fun BookingScreen( Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // Вкладки для выбора дат if (availableDates.isNotEmpty()) { - ScrollableTabRow(selectedTabIndex = availableDates.indexOf(uiState.selectedDate)) { + ScrollableTabRow( + selectedTabIndex = availableDates.indexOf(uiState.selectedDate), + ) { availableDates.forEachIndexed { index, date -> Tab( selected = date == uiState.selectedDate, @@ -50,10 +53,10 @@ fun BookingScreen( text = { Text( text = date.format(DateTimeFormatter.ofPattern("dd.MM")), - modifier = Modifier.testTag("book_date_pos_$index") + modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) ) }, - modifier = Modifier.testTag("book_date") + modifier = Modifier.testTag(TestIds.Book.ITEM_DATE) ) } } @@ -75,17 +78,17 @@ fun BookingScreen( selected = uiState.selectedPlace == place, onClick = { onSelectPlace(place) } ) - .testTag("book_place_pos_$index"), + .testTag(TestIds.Book.getIdPlaceItemByPosition(index)), verticalAlignment = Alignment.CenterVertically ) { Text( text = place, - modifier = Modifier.weight(1f).testTag("book_place_text") + modifier = Modifier.weight(1f).testTag(TestIds.Book.ITEM_PLACE_TEXT) ) RadioButton( selected = uiState.selectedPlace == place, onClick = { onSelectPlace(place) }, - modifier = Modifier.testTag("book_place_selector") + modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR) ) } } @@ -96,7 +99,7 @@ fun BookingScreen( if (availableDates.isEmpty() && !uiState.isError) { Text( text = "Всё забронировано", - modifier = Modifier.testTag("book_empty") + modifier = Modifier.testTag(TestIds.Book.EMPTY) ) } @@ -105,12 +108,12 @@ fun BookingScreen( Text( text = uiState.errorMessage ?: "Ошибка загрузки", color = Color.Red, - modifier = Modifier.testTag("book_error") + modifier = Modifier.testTag(TestIds.Book.ERROR) ) Button( onClick = onRefresh, - modifier = Modifier.testTag("book_refresh_button") + modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON) ) { Text("Обновить") } @@ -123,13 +126,13 @@ fun BookingScreen( Button( onClick = onBook, enabled = uiState.selectedPlace != null, // активна только при выбранном месте - modifier = Modifier.fillMaxWidth().testTag("book_book_button") + modifier = Modifier.fillMaxWidth().testTag(TestIds.Book.BOOK_BUTTON) ) { Text("Забронировать") } } Button( onClick = onBack, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag("book_back_button") + modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag(TestIds.Book.BACK_BUTTON) ) { Text("Назад") } -- 2.34.1 From 14cf326e0b0f85ff7dc6ad70f93ece17b6b3d213 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 3 Dec 2025 17:34:25 +0300 Subject: [PATCH 10/16] auth broken --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/repo/AuthRepository.kt | 31 +++++++++++++------ .../work/data/source/NetworkDataSource.kt | 6 ++-- .../auth/CheckAndSaveAuthCodeUseCase.kt | 20 ++++++------ .../work/ui/screen/auth/AuthViewModel.kt | 1 + 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index 5fe3adf..4a23c92 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://192.168.1.39:8080" + const val HOST = "http://172.22.21.143:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 555399d..d108bf6 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,21 +1,34 @@ package ru.myitschool.work.data.repo -import android.content.Context + import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode import ru.myitschool.work.data.source.NetworkDataSource + object AuthRepository { private var codeCache: String? = null + suspend fun checkAndSave(text: String): Result { - /* return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text - createAuthCode(code = text) + return try { + val result = NetworkDataSource.checkAuth(text) + + when { + result.isSuccess && result.getOrNull() == true -> { + codeCache = text + createAuthCode(code = text) + Result.success(true) + } + result.isFailure -> { + val exception = result.exceptionOrNull() + val errorMessage = exception?.message ?: "Ошибка авторизации" + Result.failure(Exception(errorMessage)) + } + else -> { + Result.success(false) + } } + } catch (e: Exception) { + Result.failure(e) } - } */ - codeCache = text - createAuthCode(code = text) - return Result.success(true) // TODO: ВЕРНУТЬ СЕТЕВОЙ ЗАПРОС } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 546f664..19b2c91 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -35,10 +35,12 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) // TODO: Отпрвка запроса на сервер + val response = client.get(getUrl(code, Constants.AUTH_URL)) + when (response.status) { HttpStatusCode.OK -> true - else -> error(response.bodyAsText()) + HttpStatusCode.Unauthorized, HttpStatusCode.BadRequest -> false + else -> error("Request error: ${response.bodyAsText()}") } } } diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 012fb6f..8104595 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt @@ -2,14 +2,14 @@ package ru.myitschool.work.domain.auth import ru.myitschool.work.data.repo.AuthRepository -class CheckAndSaveAuthCodeUseCase( - private val repository: AuthRepository -) { - suspend operator fun invoke( - text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") + class CheckAndSaveAuthCodeUseCase( + private val repository: AuthRepository + ) { + suspend operator fun invoke( + text: String + ): Result { + return repository.checkAndSave(text).mapCatching { success -> + if (!success) error("Code is incorrect") + } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 0a0ac5b..0aa62d2 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -38,6 +38,7 @@ class AuthViewModel : ViewModel() { }, onFailure = { error -> error.printStackTrace() + error.message?.let { Log.d("AnnaKonda", it) } if (error.message != null) { _actionFlow.emit(AuthAction.ShowError(error.message.toString())) } -- 2.34.1 From 9617ba31c305f95384d01e5a7f0007329b417a2e Mon Sep 17 00:00:00 2001 From: Dell Date: Wed, 3 Dec 2025 17:49:30 +0300 Subject: [PATCH 11/16] =?UTF-8?q?=D0=B1=D1=83=D0=BA=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../work/ui/screen/NavigationGraph.kt | 2 +- .../work/ui/screen/book/BookAction.kt | 6 + .../work/ui/screen/book/BookIntent.kt | 11 ++ .../work/ui/screen/book/BookScreen.kt | 111 ++++++++------- .../work/ui/screen/book/BookState.kt | 15 +++ .../work/ui/screen/book/BookViewModel.kt | 126 ++++++++++++++++++ .../work/ui/screen/book/BookingState.kt | 12 -- .../work/ui/screen/book/BookingViewModel.kt | 87 ------------ 8 files changed, 220 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt delete mode 100644 app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 93e99c6..ae03202 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -38,7 +38,7 @@ fun AppNavHost( composable { BookScreen( onBack = { navController.popBackStack() }, - onBookingSuccess = { + onBookSuccess = { // Возвращаемся на главный экран и обновляем его navController.popBackStack() } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt new file mode 100644 index 0000000..2bd500d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookAction.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.screen.book + +sealed interface BookAction { + data class ShowError(val message: String?) : BookAction + object BookSuccess : BookAction +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt new file mode 100644 index 0000000..4630d54 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.ui.screen.book + +import java.time.LocalDate + +sealed interface BookIntent { + object LoadData : BookIntent + object Refresh : BookIntent + object BookPlace : BookIntent + data class SelectDate(val date: LocalDate) : BookIntent + data class SelectPlace(val place: String) : BookIntent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index c18dced..25213fb 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -1,39 +1,74 @@ package ru.myitschool.work.ui.screen.book -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -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.* import androidx.compose.foundation.selection.selectable -import androidx.compose.material3.Button -import androidx.compose.material3.RadioButton -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import java.time.LocalDate import java.time.format.DateTimeFormatter -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.lifecycle.viewmodel.compose.viewModel import ru.myitschool.work.core.TestIds @Composable -fun BookingScreen( - uiState: BookingState, // состояние интерфейса - onSelectDate: (LocalDate) -> Unit, // callback при выборе даты - onSelectPlace: (String) -> Unit, // callback при выборе места - onBook: () -> Unit, // callback при бронировании - onBack: () -> Unit, // callback при нажатии "Назад" - onRefresh: () -> Unit // callback при обновлении +fun BookScreen( + onBack: () -> Unit, + onBookSuccess: () -> Unit +) { + val viewModel: BookViewModel = viewModel() + val uiState by viewModel.uiState.collectAsState() + + // Обработка действий + val event = viewModel.actionFlow.collectAsState(initial = null) + LaunchedEffect(event.value) { + when (event.value) { + is BookAction.BookSuccess -> { + onBookSuccess() + } + else -> {} + } + } + + // Загрузка начальных данных + LaunchedEffect(Unit) { + viewModel.onIntent(BookIntent.LoadData) + } + + when (uiState) { + is BookState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is BookState.Data -> { + BookContentScreen( + uiState = uiState as BookState.Data, + onSelectDate = { date -> viewModel.onIntent(BookIntent.SelectDate(date)) }, + onSelectPlace = { place -> viewModel.onIntent(BookIntent.SelectPlace(place)) }, + onBook = { viewModel.onIntent(BookIntent.BookPlace) }, + onBack = onBack, + onRefresh = { viewModel.onIntent(BookIntent.Refresh) } + ) + } + } +} + +@Composable +fun BookContentScreen( + uiState: BookState.Data, + onSelectDate: (LocalDate) -> Unit, + onSelectPlace: (String) -> Unit, + onBook: () -> Unit, + onBack: () -> Unit, + onRefresh: () -> Unit ) { // Сортировка дат по порядку val sortedDates = uiState.dates.sorted() @@ -45,7 +80,7 @@ fun BookingScreen( if (availableDates.isNotEmpty()) { ScrollableTabRow( selectedTabIndex = availableDates.indexOf(uiState.selectedDate), - ) { + ) { availableDates.forEachIndexed { index, date -> Tab( selected = date == uiState.selectedDate, @@ -137,28 +172,4 @@ fun BookingScreen( Text("Назад") } } -} - - - - -@Composable -fun BookScreen( - onBack: () -> Unit, // callback при возврате назад - onBookingSuccess: () -> Unit // callback при успешном бронировании -) { - val viewModel: BookingViewModel = BookingViewModel() - val uiState by viewModel.uiState.collectAsState() - - BookingScreen( - uiState = uiState, - onSelectDate = { date -> viewModel.selectDate(date) }, - onSelectPlace = { place -> viewModel.selectPlace(place) }, - onBook = { - viewModel.bookPlace() - onBookingSuccess() - }, - onBack = onBack, - onRefresh = { viewModel.refresh() } - ) } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt new file mode 100644 index 0000000..5677a72 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.ui.screen.book + +import java.time.LocalDate + +sealed interface BookState { + object Loading : BookState + data class Data( + val dates: List = emptyList(), + val places: Map> = emptyMap(), + val selectedDate: LocalDate? = null, + val selectedPlace: String? = null, + val isError: Boolean = false, + val errorMessage: String? = null + ) : BookState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt new file mode 100644 index 0000000..81ac5e2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,126 @@ +package ru.myitschool.work.ui.screen.book + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 java.time.LocalDate + +class BookViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(BookState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + init { + loadBookData() + } + + fun onIntent(intent: BookIntent) { + when (intent) { + is BookIntent.LoadData -> loadBookData() + is BookIntent.Refresh -> refresh() + is BookIntent.BookPlace -> bookPlace() + is BookIntent.SelectDate -> selectDate(intent.date) + is BookIntent.SelectPlace -> selectPlace(intent.place) + } + } + + private fun loadBookData() { + viewModelScope.launch(Dispatchers.IO) { + _uiState.update { BookState.Loading } + + try { + // Временные mock данные + val mockDates = listOf( + LocalDate.now().plusDays(1), + LocalDate.now().plusDays(2), + LocalDate.now().plusDays(3) + ) + + val mockPlaces = mapOf( + mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), + mockDates[1] to listOf("Место 1", "Место 2"), + mockDates[2] to listOf("Место 1") + ) + + val sortedDates = mockDates.sorted() + val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } + val defaultDate = availableDates.firstOrNull() + + _uiState.update { + BookState.Data( + dates = sortedDates, + places = mockPlaces, + selectedDate = defaultDate, + selectedPlace = null, + isError = false, + errorMessage = null + ) + } + } catch (e: Exception) { + _uiState.update { + BookState.Data( + isError = true, + errorMessage = "Ошибка загрузки данных" + ) + } + _actionFlow.emit(BookAction.ShowError("Ошибка загрузки данных")) + } + } + } + + private fun selectDate(date: LocalDate) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy( + selectedDate = date, + selectedPlace = null + ) + else -> currentState + } + } + } + + private fun selectPlace(place: String) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy(selectedPlace = place) + else -> currentState + } + } + } + + private fun bookPlace() { + viewModelScope.launch(Dispatchers.IO) { + try { + // вызов API для бронирования + // временная имитация успеха + _actionFlow.emit(BookAction.BookSuccess) + } catch (e: Exception) { + _uiState.update { currentState -> + when (currentState) { + is BookState.Data -> currentState.copy( + isError = true, + errorMessage = "Ошибка бронирования" + ) + else -> currentState + } + } + _actionFlow.emit(BookAction.ShowError("Ошибка бронирования")) + } + } + } + + private fun refresh() { + loadBookData() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt deleted file mode 100644 index b61dcc7..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -import java.time.LocalDate - -data class BookingState( - val dates: List = emptyList(), // список доступных дат - val places: Map> = emptyMap(), // места по датам - val selectedDate: LocalDate? = null, // выбранная дата - val selectedPlace: String? = null, // выбранное место - val isError: Boolean = false, // флаг ошибки - val errorMessage: String? = null // сообщение об ошибке -) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt deleted file mode 100644 index 96133c8..0000000 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookingViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -package ru.myitschool.work.ui.screen.book - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import java.time.LocalDate - -class BookingViewModel : ViewModel() { - - private val _uiState = MutableStateFlow(BookingState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - loadBookingData() - } - - fun loadBookingData() { - viewModelScope.launch { - try { - // Временные mock данные - val mockDates = listOf( - LocalDate.now().plusDays(1), - LocalDate.now().plusDays(2), - LocalDate.now().plusDays(3) - ) - - val mockPlaces = mapOf( - mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), - mockDates[1] to listOf("Место 1", "Место 2"), - mockDates[2] to listOf("Место 1") - ) - - val sortedDates = mockDates.sorted() - val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } - val defaultDate = availableDates.firstOrNull() - - _uiState.value = _uiState.value.copy( - dates = sortedDates, - places = mockPlaces, - selectedDate = defaultDate, - selectedPlace = null, - isError = false, - errorMessage = null - ) - - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - isError = true, - errorMessage = "Ошибка загрузки данных" - ) - } - } - } - - fun selectDate(date: LocalDate) { - _uiState.value = _uiState.value.copy( - selectedDate = date, - selectedPlace = null - ) - } - - fun selectPlace(place: String) { - _uiState.value = _uiState.value.copy( - selectedPlace = place - ) - } - - fun bookPlace() { - viewModelScope.launch { - try { - //вызов API для бронирования - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - isError = true, - errorMessage = "Ошибка бронирования" - ) - } - } - } - - fun refresh() { - loadBookingData() - } -} \ No newline at end of file -- 2.34.1 From 355df01846ac673816e93f73fc4a8625aa897c4b Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Dec 2025 10:50:58 +0300 Subject: [PATCH 12/16] auth with server --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/repo/AuthRepository.kt | 27 ++++---------- .../work/data/source/DataStoreDataSource.kt | 1 - .../work/data/source/NetworkDataSource.kt | 3 +- .../auth/CheckAndSaveAuthCodeUseCase.kt | 6 ++-- .../work/ui/screen/auth/AuthScreen.kt | 35 +++++++++---------- .../work/ui/screen/auth/AuthViewModel.kt | 28 +++++++-------- .../work/ui/screen/main/MainScreen.kt | 2 -- 8 files changed, 43 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index 4a23c92..faca25e 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://172.22.21.143:8080" + const val HOST = "http://10.230.214.96:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index d108bf6..0f55525 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -1,6 +1,8 @@ package ru.myitschool.work.data.repo +import android.util.Log +import io.ktor.client.statement.bodyAsText import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode import ru.myitschool.work.data.source.NetworkDataSource @@ -9,26 +11,11 @@ object AuthRepository { private var codeCache: String? = null suspend fun checkAndSave(text: String): Result { - return try { - val result = NetworkDataSource.checkAuth(text) - - when { - result.isSuccess && result.getOrNull() == true -> { - codeCache = text - createAuthCode(code = text) - Result.success(true) - } - result.isFailure -> { - val exception = result.exceptionOrNull() - val errorMessage = exception?.message ?: "Ошибка авторизации" - Result.failure(Exception(errorMessage)) - } - else -> { - Result.success(false) - } - } - } catch (e: Exception) { - Result.failure(e) + val result = NetworkDataSource.checkAuth(text) + if (result.isSuccess) { + codeCache = text + createAuthCode(code = text) } + return result } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt index 5014e2f..f0911d2 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -19,7 +19,6 @@ val AUTH_KEY = stringPreferencesKey(DS_AUTH_KEY) object DataStoreDataSource { fun authFlow(): Flow { - Log.d("AnnaKonda", "Code is checking") return App.context.dataStore.data.map { preferences -> (preferences[AUTH_KEY] ?: 0).toString() } diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 19b2c91..66c3dcf 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.data.source +import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -39,7 +40,7 @@ object NetworkDataSource { when (response.status) { HttpStatusCode.OK -> true - HttpStatusCode.Unauthorized, HttpStatusCode.BadRequest -> false + HttpStatusCode.Unauthorized -> error("Wrong code!") else -> error("Request error: ${response.bodyAsText()}") } } diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 8104595..06ae55b 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt @@ -7,9 +7,7 @@ import ru.myitschool.work.data.repo.AuthRepository ) { suspend operator fun invoke( text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") - } + ): Result { + return repository.checkAndSave(text) } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 86fc203..a66ff49 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -46,16 +46,7 @@ fun AuthScreen( viewModel.onIntent(AuthIntent.CheckLogIntent) } - val event = viewModel.actionFlow.collectAsState(initial = null) - LaunchedEffect(event.value) { - if (event.value is AuthAction.LogIn) { - if ((event.value as AuthAction.LogIn).isLogged) { - navController.navigate(MainScreenDestination) - } - } - } - Log.d("AnnaKonda", state.javaClass.toString()) Column( modifier = Modifier .fillMaxSize() @@ -69,7 +60,7 @@ fun AuthScreen( textAlign = TextAlign.Center ) when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) + is AuthState.Data -> Content(viewModel, currentState, navController) is AuthState.Loading -> { CircularProgressIndicator( modifier = Modifier.size(64.dp) @@ -86,7 +77,8 @@ fun AuthScreen( @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState.Data, + navController: NavController ) { var inputText by remember { mutableStateOf("") } var errorText: String? by remember { mutableStateOf(null) } @@ -94,15 +86,22 @@ private fun Content( val event = viewModel.actionFlow.collectAsState(initial = null) - LaunchedEffect(event.value) { - if (event.value is AuthAction.ShowError) { - errorText = (event.value as AuthAction.ShowError).message - } else if (event.value is AuthAction.AuthBtnEnabled){ - Log.d("AnnaKonda", btnEnabled.toString()) - btnEnabled = if ((event.value as AuthAction.AuthBtnEnabled).enabled){ true } else { false } + // В UI (Composable) + val actionFlow = viewModel.actionFlow // SharedFlow + + LaunchedEffect(Unit) { + // Collect Flow здесь, чтобы потреблять все события + actionFlow.collect { action -> + when (action) { + is AuthAction.ShowError -> { + errorText = action.message + } + is AuthAction.AuthBtnEnabled -> { + btnEnabled = action.enabled + } else -> {} + } } } - Spacer(modifier = Modifier.size(16.dp)) TextField( modifier = Modifier diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 0aa62d2..97bd07a 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.App @@ -24,7 +25,7 @@ class AuthViewModel : ViewModel() { private val _uiState = MutableStateFlow(AuthState.Data) val uiState: StateFlow = _uiState.asStateFlow() - private val _actionFlow: MutableSharedFlow = MutableSharedFlow() + private val _actionFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) val actionFlow: SharedFlow = _actionFlow fun onIntent(intent: AuthIntent) { @@ -38,17 +39,17 @@ class AuthViewModel : ViewModel() { }, onFailure = { error -> error.printStackTrace() - error.message?.let { Log.d("AnnaKonda", it) } if (error.message != null) { - _actionFlow.emit(AuthAction.ShowError(error.message.toString())) + _actionFlow.emit(AuthAction.ShowError(error.message)) + _uiState.update { AuthState.Data } } - _uiState.update { AuthState.Data } + } ) } } - is AuthIntent.TextInput -> { + is AuthIntent.TextInput -> { viewModelScope.launch { authFlow().collect { if (CheckCodeInput(intent.text)) { @@ -59,19 +60,18 @@ class AuthViewModel : ViewModel() { } } } + is AuthIntent.CheckLogIntent -> { viewModelScope.launch { _uiState.update { AuthState.Loading } - authFlow().collect { - Log.d("AnnaKonda", it) - if (it != "0") { - _actionFlow.emit(AuthAction.LogIn(true)) - _uiState.update { AuthState.LoggedIn } - } else { - _actionFlow.emit(AuthAction.ShowError(App.context.getString(R.string.auth_wrong_code))) - _uiState.update { AuthState.Data } - } + val authCode = authFlow().first() + if (authCode != "0") { + _actionFlow.emit(AuthAction.LogIn(true)) + _uiState.update { AuthState.LoggedIn } + } else { + _uiState.update { AuthState.Data } } + } } } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 5460355..6bc96dc 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -39,7 +39,6 @@ fun MainScreen( errorMessage = (event.value as MainAction.ShowError).message } } - Log.d("AnnaKonda", errorMessage.toString()) // Если ошибка - показываем только ошибку и кнопку обновления if (errorMessage != null) { ErrorScreen(viewModel = viewModel, navController = navController, errorMessage) @@ -87,7 +86,6 @@ fun DefaultScreen(viewModel: MainViewModel, verticalAlignment = Alignment.CenterVertically ) { // Фото пользователя (main_photo) - employee?.photoUrl?.let { msg -> Log.d("AnnaKonda", msg) } AsyncImage( model = employee?.photoUrl ?: "", contentDescription = "Фото", -- 2.34.1 From ab03679731f3077d721d8f416ccd28be556aa44b Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Dec 2025 11:52:16 +0300 Subject: [PATCH 13/16] booking with server --- .../work/data/repo/BookingRepository.kt | 17 +++ .../work/data/source/NetworkDataSource.kt | 32 +++++ .../book/GetAvailableBookingsUseCase.kt | 13 ++ .../work/ui/screen/book/BookIntent.kt | 3 +- .../work/ui/screen/book/BookScreen.kt | 5 +- .../work/ui/screen/book/BookState.kt | 5 +- .../work/ui/screen/book/BookViewModel.kt | 127 +++++++++--------- 7 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/book/GetAvailableBookingsUseCase.kt diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt new file mode 100644 index 0000000..b16e8c4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt @@ -0,0 +1,17 @@ +package ru.myitschool.work.data.repo + +import ru.myitschool.work.data.entity.Place +import ru.myitschool.work.data.source.DataStoreDataSource +import ru.myitschool.work.data.source.NetworkDataSource +import java.time.LocalDate + +class BookingRepository { + + suspend fun getAvailableBookings(): Result>> { + val code = DataStoreDataSource.getAuthCode() + if (code.isEmpty() || code == "0") { + return Result.failure(Exception("Auth code not found")) + } + return NetworkDataSource.getAvailableBookings(code) + } +} diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 66c3dcf..2127f9d 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -109,5 +109,37 @@ object NetworkDataSource { } } } + + suspend fun getAvailableBookings(code: String): Result>> = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + + when (response.status) { + HttpStatusCode.OK -> { + val json = response.bodyAsText() + val jsonObject = Json.parseToJsonElement(json).jsonObject + val availableBookings = mutableMapOf>() + + for ((dateString, placesArray) in jsonObject) { + val date = LocalDate.parse(dateString) + val places = placesArray.jsonArray.map { placeElement -> + val placeObj = placeElement.jsonObject + val id = placeObj["id"]?.jsonPrimitive?.long + ?: error("Missing 'id' in place") + val placeName = placeObj["place"]?.jsonPrimitive?.content + ?: error("Missing 'place' in place") + Place(id, placeName) + } + if (places.isNotEmpty()) { + availableBookings[date] = places + } + } + availableBookings.toSortedMap() + } + + else -> error("Request error: ${response.bodyAsText()}") + } + } + } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/book/GetAvailableBookingsUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/book/GetAvailableBookingsUseCase.kt new file mode 100644 index 0000000..cc894c7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/GetAvailableBookingsUseCase.kt @@ -0,0 +1,13 @@ +package ru.myitschool.work.domain.book + +import ru.myitschool.work.data.entity.Place +import ru.myitschool.work.data.repo.BookingRepository +import java.time.LocalDate + +class GetAvailableBookingsUseCase( + private val repository: BookingRepository +) { + suspend operator fun invoke(): Result>> { + return repository.getAvailableBookings() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt index 4630d54..62744be 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.book +import ru.myitschool.work.data.entity.Place import java.time.LocalDate sealed interface BookIntent { @@ -7,5 +8,5 @@ sealed interface BookIntent { object Refresh : BookIntent object BookPlace : BookIntent data class SelectDate(val date: LocalDate) : BookIntent - data class SelectPlace(val place: String) : BookIntent + data class SelectPlace(val place: Place) : BookIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 25213fb..71c38ca 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import java.time.LocalDate import java.time.format.DateTimeFormatter import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.entity.Place @Composable fun BookScreen( @@ -65,7 +66,7 @@ fun BookScreen( fun BookContentScreen( uiState: BookState.Data, onSelectDate: (LocalDate) -> Unit, - onSelectPlace: (String) -> Unit, + onSelectPlace: (Place) -> Unit, onBook: () -> Unit, onBack: () -> Unit, onRefresh: () -> Unit @@ -117,7 +118,7 @@ fun BookContentScreen( verticalAlignment = Alignment.CenterVertically ) { Text( - text = place, + text = place.place, modifier = Modifier.weight(1f).testTag(TestIds.Book.ITEM_PLACE_TEXT) ) RadioButton( diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt index 5677a72..8163b48 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -1,14 +1,15 @@ package ru.myitschool.work.ui.screen.book +import ru.myitschool.work.data.entity.Place import java.time.LocalDate sealed interface BookState { object Loading : BookState data class Data( val dates: List = emptyList(), - val places: Map> = emptyMap(), + val places: Map> = emptyMap(), val selectedDate: LocalDate? = null, - val selectedPlace: String? = null, + val selectedPlace: Place? = null, val isError: Boolean = false, val errorMessage: String? = null ) : BookState diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index 81ac5e2..e248b35 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -7,12 +7,21 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ru.myitschool.work.data.entity.Place +import ru.myitschool.work.data.repo.BookingRepository +// import ru.myitschool.work.domain.book.CreateBookingUseCase +import ru.myitschool.work.domain.book.GetAvailableBookingsUseCase import java.time.LocalDate class BookViewModel : ViewModel() { + private val repository by lazy { BookingRepository() } + private val getAvailableBookingsUseCase by lazy { GetAvailableBookingsUseCase(repository) } + // private val createBookingUseCase by lazy { CreateBookingUseCase(repository) } + private val _uiState = MutableStateFlow(BookState.Loading) val uiState: StateFlow = _uiState.asStateFlow() @@ -20,6 +29,8 @@ class BookViewModel : ViewModel() { private val _actionFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + private var selectedPlaceId: Long? = null + init { loadBookData() } @@ -38,89 +49,85 @@ class BookViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { _uiState.update { BookState.Loading } - try { - // Временные mock данные - val mockDates = listOf( - LocalDate.now().plusDays(1), - LocalDate.now().plusDays(2), - LocalDate.now().plusDays(3) - ) - - val mockPlaces = mapOf( - mockDates[0] to listOf("Место 1", "Место 2", "Место 3"), - mockDates[1] to listOf("Место 1", "Место 2"), - mockDates[2] to listOf("Место 1") - ) - - val sortedDates = mockDates.sorted() - val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true } - val defaultDate = availableDates.firstOrNull() - - _uiState.update { - BookState.Data( - dates = sortedDates, - places = mockPlaces, - selectedDate = defaultDate, - selectedPlace = null, - isError = false, - errorMessage = null - ) + getAvailableBookingsUseCase().fold( + onSuccess = { bookings -> + if (bookings.isEmpty()) { + _uiState.update { + BookState.Data( + isError = true, + errorMessage = "Нет доступных дат для бронирования" + ) + } + } else { + val dates = bookings.keys.toList() + _uiState.update { + BookState.Data( + dates = dates, + places = bookings, + selectedDate = dates.first(), + selectedPlace = null, + isError = false, + errorMessage = null + ) + } + } + }, + onFailure = { error -> + error.printStackTrace() + _uiState.update { + BookState.Data( + isError = true, + errorMessage = error.message ?: "Ошибка загрузки данных" + ) + } } - } catch (e: Exception) { - _uiState.update { - BookState.Data( - isError = true, - errorMessage = "Ошибка загрузки данных" - ) - } - _actionFlow.emit(BookAction.ShowError("Ошибка загрузки данных")) - } + ) } } private fun selectDate(date: LocalDate) { _uiState.update { currentState -> - when (currentState) { - is BookState.Data -> currentState.copy( + if (currentState is BookState.Data) { + currentState.copy( selectedDate = date, selectedPlace = null ) - else -> currentState + } else { + currentState } } + selectedPlaceId = null } - private fun selectPlace(place: String) { + private fun selectPlace(place: Place) { _uiState.update { currentState -> - when (currentState) { - is BookState.Data -> currentState.copy(selectedPlace = place) - else -> currentState + if (currentState is BookState.Data) { + currentState.copy(selectedPlace = place) + } else { + currentState } } + selectedPlaceId = place.id } private fun bookPlace() { - viewModelScope.launch(Dispatchers.IO) { - try { - // вызов API для бронирования - // временная имитация успеха - _actionFlow.emit(BookAction.BookSuccess) - } catch (e: Exception) { - _uiState.update { currentState -> - when (currentState) { - is BookState.Data -> currentState.copy( - isError = true, - errorMessage = "Ошибка бронирования" - ) - else -> currentState + /* + selectedPlaceId?.let { placeId -> + viewModelScope.launch(Dispatchers.IO) { + createBookingUseCase(placeId).fold( + onSuccess = { + _actionFlow.emit(BookAction.BookSuccess) + }, + onFailure = { error -> + error.printStackTrace() + _actionFlow.emit(BookAction.ShowError(error.message ?: "Ошибка бронирования")) } - } - _actionFlow.emit(BookAction.ShowError("Ошибка бронирования")) + ) } - } + }*/ } private fun refresh() { loadBookData() } -} \ No newline at end of file +} -- 2.34.1 From 60198e13cb00104b78f8aee14f4fc9c782c6c3d0 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Dec 2025 12:07:22 +0300 Subject: [PATCH 14/16] booking post without server-test --- .../work/data/repo/BookingRepository.kt | 8 ++++++ .../work/data/source/NetworkDataSource.kt | 25 +++++++++++++++++++ .../work/domain/book/CreateBookingUseCase.kt | 12 +++++++++ .../work/ui/screen/book/BookViewModel.kt | 16 +++++++----- 4 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/ru/myitschool/work/domain/book/CreateBookingUseCase.kt diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt index b16e8c4..d8251bd 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt @@ -14,4 +14,12 @@ class BookingRepository { } return NetworkDataSource.getAvailableBookings(code) } + + suspend fun createBooking(date: LocalDate, placeId: Long): Result { + val code = DataStoreDataSource.getAuthCode() + if (code.isEmpty() || code == "0") { + return Result.failure(Exception("Auth code not found")) + } + return NetworkDataSource.createBooking(code, date, placeId) + } } diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 2127f9d..14a3465 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -5,11 +5,16 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants import ru.myitschool.work.data.entity.Employee @@ -141,5 +146,25 @@ object NetworkDataSource { } } } + + @Serializable + private data class CreateBookingBody(val date: String, val placeID: Long) + + suspend fun createBooking(code: String, date: LocalDate, placeId: Long): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + // Формируем тело запроса + val requestBody = CreateBookingBody(date.toString(), placeId) + + val response = client.post(getUrl(code, Constants.BOOKING_URL)) { // Используем ту же константу BOOKING_URL + contentType(ContentType.Application.Json) + setBody(requestBody) + } + + when (response.status) { + HttpStatusCode.OK -> true + else -> error("Ошибка бронирования: ${response.bodyAsText()}") + } + } + } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/book/CreateBookingUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/book/CreateBookingUseCase.kt new file mode 100644 index 0000000..99cebe3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/book/CreateBookingUseCase.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.domain.book + +import ru.myitschool.work.data.repo.BookingRepository +import java.time.LocalDate + +class CreateBookingUseCase( + private val repository: BookingRepository +) { + suspend operator fun invoke(date: LocalDate, placeId: Long): Result { + return repository.createBooking(date, placeId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index e248b35..43f6afa 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -13,14 +13,14 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.data.entity.Place import ru.myitschool.work.data.repo.BookingRepository -// import ru.myitschool.work.domain.book.CreateBookingUseCase +import ru.myitschool.work.domain.book.CreateBookingUseCase import ru.myitschool.work.domain.book.GetAvailableBookingsUseCase import java.time.LocalDate class BookViewModel : ViewModel() { private val repository by lazy { BookingRepository() } private val getAvailableBookingsUseCase by lazy { GetAvailableBookingsUseCase(repository) } - // private val createBookingUseCase by lazy { CreateBookingUseCase(repository) } + private val createBookingUseCase by lazy { CreateBookingUseCase(repository) } private val _uiState = MutableStateFlow(BookState.Loading) @@ -111,10 +111,14 @@ class BookViewModel : ViewModel() { } private fun bookPlace() { - /* - selectedPlaceId?.let { placeId -> + // Раскомментируйте и измените этот блок + val currentState = _uiState.value + if (currentState is BookState.Data && currentState.selectedPlace != null && currentState.selectedDate != null) { + val placeId = selectedPlaceId ?: return // Дополнительная проверка + val date = currentState.selectedDate + viewModelScope.launch(Dispatchers.IO) { - createBookingUseCase(placeId).fold( + createBookingUseCase (date, placeId).fold( onSuccess = { _actionFlow.emit(BookAction.BookSuccess) }, @@ -124,7 +128,7 @@ class BookViewModel : ViewModel() { } ) } - }*/ + } } private fun refresh() { -- 2.34.1 From 39f709f2807b339222f02b8a805f62c9accb5cb3 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 4 Dec 2025 13:13:55 +0300 Subject: [PATCH 15/16] all done v1 --- .../work/data/source/NetworkDataSource.kt | 42 +++++++++++-------- .../work/ui/screen/book/BookViewModel.kt | 11 ++--- .../work/ui/screen/main/MainViewModel.kt | 8 ++-- app/src/main/res/values/strings.xml | 19 +++++++++ 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 14a3465..8bbab74 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -19,6 +19,8 @@ import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants import ru.myitschool.work.data.entity.Employee import kotlinx.serialization.json.* +import ru.myitschool.work.App +import ru.myitschool.work.R import ru.myitschool.work.data.entity.Booking import ru.myitschool.work.data.entity.Place import java.time.LocalDate @@ -45,11 +47,12 @@ object NetworkDataSource { when (response.status) { HttpStatusCode.OK -> true - HttpStatusCode.Unauthorized -> error("Wrong code!") - else -> error("Request error: ${response.bodyAsText()}") + HttpStatusCode.Unauthorized -> error(App.context.getString(R.string.auth_wrong_code)) + else -> error(App.context.getString(R.string.error_request, response.bodyAsText())) } } } + suspend fun getUserInfo(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.INFO_URL)) @@ -58,21 +61,21 @@ object NetworkDataSource { HttpStatusCode.OK -> { val json = response.bodyAsText() if (json.isBlank()) { - error("Пустой ответ от сервера") + error(App.context.getString(R.string.error_empty_server_response)) } val jsonObject = try { Json.parseToJsonElement(json).jsonObject } catch (e: Exception) { - error("Ошибка парсинга: ${e.message}") + error(App.context.getString(R.string.error_parsing, e.message)) } val name = jsonObject["name"]?.jsonPrimitive?.content - ?: error("Отсутствует поле 'name'") + ?: error(App.context.getString(R.string.error_missing_name_field)) val photoUrl = jsonObject["photoUrl"]?.jsonPrimitive?.content - ?: error("Отсутствует поле 'photoUrl'") + ?: error(App.context.getString(R.string.error_missing_photo_url_field)) val bookingJson = jsonObject["booking"]?.jsonObject - ?: error("Отсутствует поле 'booking' в ответе") + ?: error(App.context.getString(R.string.error_missing_booking_field)) val employee = Employee( name = name, @@ -85,12 +88,12 @@ object NetworkDataSource { val date = LocalDate.parse(dateString) val bookingObj = bookingElement.jsonObject val bookingId = bookingObj["id"]?.jsonPrimitive?.long - ?: error("Отсутствует поле id") + ?: error(App.context.getString(R.string.error_missing_id_field)) val placeString = bookingObj["place"]?.jsonPrimitive?.content - ?: error("Отсутствует поле 'place' $dateString") + ?: error(App.context.getString(R.string.error_missing_place_field, dateString)) if (placeString.isBlank()) { - error("Пустое поле 'place' $dateString") + error(App.context.getString(R.string.error_empty_place_field, dateString)) } val placeId = bookingId @@ -104,9 +107,9 @@ object NetworkDataSource { ) bookingList.add(booking) } - if (bookingList.isEmpty()) { - error("Список бронирований пуст") - } + /* if (bookingList.isEmpty()) { + error(App.context.getString(R.string.error_booking_list_empty)) + }*/ employee.bookingList.addAll(bookingList) employee } @@ -130,9 +133,9 @@ object NetworkDataSource { val places = placesArray.jsonArray.map { placeElement -> val placeObj = placeElement.jsonObject val id = placeObj["id"]?.jsonPrimitive?.long - ?: error("Missing 'id' in place") + ?: error(App.context.getString(R.string.error_missing_id_in_place)) val placeName = placeObj["place"]?.jsonPrimitive?.content - ?: error("Missing 'place' in place") + ?: error(App.context.getString(R.string.error_missing_place_in_place)) Place(id, placeName) } if (places.isNotEmpty()) { @@ -142,7 +145,7 @@ object NetworkDataSource { availableBookings.toSortedMap() } - else -> error("Request error: ${response.bodyAsText()}") + else -> error(App.context.getString(R.string.error_request, response.bodyAsText())) } } } @@ -162,9 +165,12 @@ object NetworkDataSource { when (response.status) { HttpStatusCode.OK -> true - else -> error("Ошибка бронирования: ${response.bodyAsText()}") + else -> { + val errorBody = response.bodyAsText() + error(if (errorBody.isNotBlank()) App.context.getString(R.string.error_booking, errorBody) else App.context.getString(R.string.error_booking_default)) + } } } } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index 43f6afa..c91a5eb 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import ru.myitschool.work.App +import ru.myitschool.work.R import ru.myitschool.work.data.entity.Place import ru.myitschool.work.data.repo.BookingRepository import ru.myitschool.work.domain.book.CreateBookingUseCase @@ -55,7 +57,7 @@ class BookViewModel : ViewModel() { _uiState.update { BookState.Data( isError = true, - errorMessage = "Нет доступных дат для бронирования" + errorMessage = App.context.getString(R.string.error_no_available_dates) ) } } else { @@ -77,7 +79,7 @@ class BookViewModel : ViewModel() { _uiState.update { BookState.Data( isError = true, - errorMessage = error.message ?: "Ошибка загрузки данных" + errorMessage = error.message ?: App.context.getString(R.string.error_loading_data) ) } } @@ -111,10 +113,9 @@ class BookViewModel : ViewModel() { } private fun bookPlace() { - // Раскомментируйте и измените этот блок val currentState = _uiState.value if (currentState is BookState.Data && currentState.selectedPlace != null && currentState.selectedDate != null) { - val placeId = selectedPlaceId ?: return // Дополнительная проверка + val placeId = selectedPlaceId ?: return val date = currentState.selectedDate viewModelScope.launch(Dispatchers.IO) { @@ -124,7 +125,7 @@ class BookViewModel : ViewModel() { }, onFailure = { error -> error.printStackTrace() - _actionFlow.emit(BookAction.ShowError(error.message ?: "Ошибка бронирования")) + _actionFlow.emit(BookAction.ShowError(error.message ?: App.context.getString(R.string.error_booking_default))) } ) } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt index 0b3b065..0355811 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -19,9 +19,6 @@ import ru.myitschool.work.ui.screen.auth.AuthIntent import ru.myitschool.work.ui.screen.auth.AuthState class MainViewModel : ViewModel() { - init { - loadData() - } private val repository by lazy{ MainRepository() } private val getUserDataUseCase by lazy { GetUserDataUseCase(repository) } @@ -31,6 +28,10 @@ class MainViewModel : ViewModel() { private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + init { + loadData() + } + fun onIntent(intent: MainIntent) { when (intent) { is MainIntent.LoadData -> { @@ -59,6 +60,7 @@ class MainViewModel : ViewModel() { if (error.message != null) { _actionFlow.emit(MainAction.ShowError(error.message.toString())) } + _uiState.update { MainState.Data(null) } } ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44675a7..874bb26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,4 +6,23 @@ Войти Введён неверный код Неправильный формат кода + + Ошибка запроса + Пустой ответ от сервера + Ошибка парсинга + В ответе отсутствует поле name + В ответе отсутствует поле photoUrl + В ответе отсутствует поле booking + В ответе отсутствует поле id + В ответе отсутствует поле place для даты + В ответе поле place пусто для даты + Список бронирований пуст + В информации о месте отсутствует id + В информации о месте отсутствует place + Ошибка бронирования + Ошибка бронирования + + Нет доступных дат для бронирования + Ошибка загрузки данных + \ No newline at end of file -- 2.34.1 From 77f27e52eb4974e414b2958e26e6bd67cc8e1f08 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 5 Dec 2025 21:33:40 +0300 Subject: [PATCH 16/16] final --- .../java/ru/myitschool/work/core/Constants.kt | 2 +- .../work/data/source/NetworkDataSource.kt | 7 +-- .../work/ui/screen/book/BookScreen.kt | 46 ++++++++++--------- .../work/ui/screen/book/BookViewModel.kt | 14 +++++- .../work/ui/screen/main/MainScreen.kt | 3 +- .../work/ui/screen/main/MainViewModel.kt | 15 ++---- 6 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index faca25e..5fe3adf 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,7 +1,7 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://10.230.214.96:8080" + const val HOST = "http://192.168.1.39:8080" const val AUTH_URL = "/auth" const val INFO_URL = "/info" const val BOOKING_URL = "/booking" diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index 8bbab74..732dfa5 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -1,5 +1,6 @@ package ru.myitschool.work.data.source +import android.annotation.SuppressLint import android.util.Log import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO @@ -151,20 +152,20 @@ object NetworkDataSource { } @Serializable - private data class CreateBookingBody(val date: String, val placeID: Long) + private data class CreateBookingBody(val date: String, val placeId: Long) suspend fun createBooking(code: String, date: LocalDate, placeId: Long): Result = withContext(Dispatchers.IO) { return@withContext runCatching { // Формируем тело запроса val requestBody = CreateBookingBody(date.toString(), placeId) - val response = client.post(getUrl(code, Constants.BOOKING_URL)) { // Используем ту же константу BOOKING_URL + val response = client.post(getUrl(code, Constants.BOOK_URL)) { contentType(ContentType.Application.Json) setBody(requestBody) } when (response.status) { - HttpStatusCode.OK -> true + HttpStatusCode.Created -> true else -> { val errorBody = response.bodyAsText() error(if (errorBody.isNotBlank()) App.context.getString(R.string.error_booking, errorBody) else App.context.getString(R.string.error_booking_default)) diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 71c38ca..461cb78 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -1,19 +1,34 @@ package ru.myitschool.work.ui.screen.book -import androidx.compose.foundation.layout.* +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.selection.selectable -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.RadioButton +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +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.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import java.time.LocalDate -import java.time.format.DateTimeFormatter import ru.myitschool.work.core.TestIds import ru.myitschool.work.data.entity.Place +import java.time.LocalDate +import java.time.format.DateTimeFormatter @Composable fun BookScreen( @@ -23,23 +38,19 @@ fun BookScreen( val viewModel: BookViewModel = viewModel() val uiState by viewModel.uiState.collectAsState() - // Обработка действий - val event = viewModel.actionFlow.collectAsState(initial = null) - LaunchedEffect(event.value) { - when (event.value) { - is BookAction.BookSuccess -> { + LaunchedEffect(viewModel.actionFlow) { + viewModel.actionFlow.collect { action -> + if (action is BookAction.BookSuccess) { onBookSuccess() } - else -> {} } } - // Загрузка начальных данных LaunchedEffect(Unit) { viewModel.onIntent(BookIntent.LoadData) } - when (uiState) { + when (val state = uiState) { is BookState.Loading -> { Box( modifier = Modifier.fillMaxSize(), @@ -51,7 +62,7 @@ fun BookScreen( is BookState.Data -> { BookContentScreen( - uiState = uiState as BookState.Data, + uiState = state, onSelectDate = { date -> viewModel.onIntent(BookIntent.SelectDate(date)) }, onSelectPlace = { place -> viewModel.onIntent(BookIntent.SelectPlace(place)) }, onBook = { viewModel.onIntent(BookIntent.BookPlace) }, @@ -71,13 +82,10 @@ fun BookContentScreen( onBack: () -> Unit, onRefresh: () -> Unit ) { - // Сортировка дат по порядку val sortedDates = uiState.dates.sorted() - // Фильтрация дат, для которых есть доступные места val availableDates = sortedDates.filter { date -> uiState.places[date]?.isNotEmpty() == true } Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { - // Вкладки для выбора дат if (availableDates.isNotEmpty()) { ScrollableTabRow( selectedTabIndex = availableDates.indexOf(uiState.selectedDate), @@ -100,7 +108,6 @@ fun BookContentScreen( Spacer(modifier = Modifier.height(16.dp)) - // Список мест для выбранной даты val placesForDate = uiState.selectedDate?.let { uiState.places[it] } ?: emptyList() if (placesForDate.isNotEmpty()) { @@ -131,7 +138,6 @@ fun BookContentScreen( } } - // пустой список (все забронировано) if (availableDates.isEmpty() && !uiState.isError) { Text( text = "Всё забронировано", @@ -139,7 +145,6 @@ fun BookContentScreen( ) } - // ошибка if (uiState.isError) { Text( text = uiState.errorMessage ?: "Ошибка загрузки", @@ -157,7 +162,6 @@ fun BookContentScreen( Spacer(modifier = Modifier.weight(1f)) - // Кнопки: Забронировать и Назад if (!uiState.isError && placesForDate.isNotEmpty()) { Button( onClick = onBook, diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt index c91a5eb..b1b6e4f 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -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 @@ -121,11 +122,22 @@ class BookViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { createBookingUseCase (date, placeId).fold( onSuccess = { + Log.d("AnnaKonda", "method is calling") _actionFlow.emit(BookAction.BookSuccess) }, onFailure = { error -> + Log.d("AnnaKonda", "ERROR method is calling") error.printStackTrace() - _actionFlow.emit(BookAction.ShowError(error.message ?: App.context.getString(R.string.error_booking_default))) + _uiState.update { currentState -> + if (currentState is BookState.Data) { + currentState.copy( + isError = true + ) + } else { + currentState + } + } + // _actionFlow.emit(BookAction.ShowError(error.message ?: App.context.getString(R.string.error_booking_default))) } ) } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt index 6bc96dc..38da245 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.AsyncImage import ru.myitschool.work.core.TestIds @@ -25,7 +26,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination fun MainScreen( navController: NavController, ) { - val viewModel = MainViewModel() + val viewModel: MainViewModel = viewModel() // Состояния val event = viewModel.actionFlow.collectAsState(initial = null) // Функция загрузки данных diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt index 0355811..8a3f5bb 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -12,14 +12,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.MainRepository -import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase import ru.myitschool.work.domain.main.GetUserDataUseCase -import ru.myitschool.work.ui.screen.auth.AuthAction -import ru.myitschool.work.ui.screen.auth.AuthIntent -import ru.myitschool.work.ui.screen.auth.AuthState class MainViewModel : ViewModel() { - private val repository by lazy{ MainRepository() } + private val repository by lazy { MainRepository() } private val getUserDataUseCase by lazy { GetUserDataUseCase(repository) } private val _uiState = MutableStateFlow(MainState.Loading) @@ -28,15 +24,12 @@ class MainViewModel : ViewModel() { private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow - init { - loadData() - } - fun onIntent(intent: MainIntent) { when (intent) { - is MainIntent.LoadData -> { + is MainIntent.LoadData -> { loadData() } + is MainIntent.LogOut -> { viewModelScope.launch(Dispatchers.IO) { _uiState.update { MainState.Data(null) } @@ -46,7 +39,7 @@ class MainViewModel : ViewModel() { } } - fun loadData() { + private fun loadData() { viewModelScope.launch(Dispatchers.IO) { _uiState.update { MainState.Loading } -- 2.34.1