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/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/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/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/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..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 @@ -1,16 +1,21 @@ 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 -> + /* return NetworkDataSource.checkAuth(text).onSuccess { success -> if (success) { codeCache = 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/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 new file mode 100644 index 0000000..5014e2f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -0,0 +1,49 @@ +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.first +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(): Flow { + Log.d("AnnaKonda", "Code is checking") + return App.context.dataStore.data.map { preferences -> + (preferences[AUTH_KEY] ?: 0).toString() + } + } + + suspend fun createAuthCode(code: String) { + App.context.dataStore.updateData { + it.toMutablePreferences().also { preferences -> + preferences[AUTH_KEY] = code + } + } + } + + 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) + } + } + } +} \ 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..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 { @@ -30,13 +35,76 @@ 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()) } } } + 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/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 5577ca7..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 @@ -8,10 +8,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 @@ -31,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/AuthAction.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt new file mode 100644 index 0000000..fbab1c2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthAction.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.auth + +sealed interface AuthAction { + data class ShowError(val message: String?) : 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/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 c9f4d5f..86fc203 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 @@ -27,8 +28,12 @@ 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 +import ru.myitschool.work.core.Utils import ru.myitschool.work.ui.nav.MainScreenDestination @Composable @@ -37,13 +42,20 @@ fun AuthScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } + 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() @@ -63,6 +75,10 @@ fun AuthScreen( modifier = Modifier.size(64.dp) ) } + + is AuthState.LoggedIn -> { + navController.navigate(MainScreenDestination) + } } } } @@ -73,9 +89,25 @@ private fun Content( state: AuthState.Data ) { 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 } + } + } + 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 @@ -85,14 +117,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 = { - viewModel.onIntent(AuthIntent.Send(inputText)) + if (Utils.CheckCodeInput(inputText)) { + viewModel.onIntent(AuthIntent.Send(inputText)) + } else { + errorText = App.context.getString(R.string.auth_nasty_code) + } }, - enabled = true - - ) { Text(stringResource(R.string.auth_sign_in)) - + enabled = btnEnabled + ) { Text(stringResource(R.string.auth_sign_in)) } + 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/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 3153640..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 @@ -1,5 +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 @@ -10,7 +12,11 @@ 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.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 class AuthViewModel : ViewModel() { @@ -18,26 +24,55 @@ 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) + _uiState.update { AuthState.LoggedIn } }, onFailure = { error -> error.printStackTrace() - _actionFlow.emit(Unit) + if (error.message != null) { + _actionFlow.emit(AuthAction.ShowError(error.message.toString())) + } + _uiState.update { AuthState.Data } } ) } } - 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(true)) + _uiState.update { AuthState.LoggedIn } + } else { + _actionFlow.emit(AuthAction.ShowError(App.context.getString(R.string.auth_wrong_code))) + _uiState.update { AuthState.Data } + } + } + } + } } } } \ 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 c64acee..c18dced 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 @@ -28,7 +28,7 @@ import ru.myitschool.work.core.TestIds @Composable fun BookingScreen( - uiState: BookingUiState, // состояние интерфейса + uiState: BookingState, // состояние интерфейса onSelectDate: (LocalDate) -> Unit, // callback при выборе даты onSelectPlace: (String) -> Unit, // callback при выборе места onBook: () -> Unit, // callback при бронировании @@ -139,22 +139,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 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