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..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,16 +1,21 @@ 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 -object AuthRepository { +object AuthRepository { private var codeCache: String? = null suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text - } + 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/repo/BookingRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt new file mode 100644 index 0000000..d8251bd --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookingRepository.kt @@ -0,0 +1,25 @@ +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) + } + + 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/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..f0911d2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/source/DataStoreDataSource.kt @@ -0,0 +1,48 @@ +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 { + 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..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,16 +1,30 @@ 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 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 +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 object NetworkDataSource { private val client by lazy { @@ -31,12 +45,133 @@ object NetworkDataSource { suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { val response = client.get(getUrl(code, Constants.AUTH_URL)) + when (response.status) { HttpStatusCode.OK -> true + 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)) + + when (response.status) { + HttpStatusCode.OK -> { + val json = response.bodyAsText() + if (json.isBlank()) { + error(App.context.getString(R.string.error_empty_server_response)) + } + + val jsonObject = try { + Json.parseToJsonElement(json).jsonObject + } catch (e: Exception) { + error(App.context.getString(R.string.error_parsing, e.message)) + } + val name = jsonObject["name"]?.jsonPrimitive?.content + ?: error(App.context.getString(R.string.error_missing_name_field)) + val photoUrl = jsonObject["photoUrl"]?.jsonPrimitive?.content + ?: error(App.context.getString(R.string.error_missing_photo_url_field)) + + val bookingJson = jsonObject["booking"]?.jsonObject + ?: error(App.context.getString(R.string.error_missing_booking_field)) + + 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(App.context.getString(R.string.error_missing_id_field)) + val placeString = bookingObj["place"]?.jsonPrimitive?.content + ?: error(App.context.getString(R.string.error_missing_place_field, dateString)) + + if (placeString.isBlank()) { + error(App.context.getString(R.string.error_empty_place_field, 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(App.context.getString(R.string.error_booking_list_empty)) + }*/ + employee.bookingList.addAll(bookingList) + employee + } else -> error(response.bodyAsText()) } } } + 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(App.context.getString(R.string.error_missing_id_in_place)) + val placeName = placeObj["place"]?.jsonPrimitive?.content + ?: error(App.context.getString(R.string.error_missing_place_in_place)) + Place(id, placeName) + } + if (places.isNotEmpty()) { + availableBookings[date] = places + } + } + availableBookings.toSortedMap() + } + + else -> error(App.context.getString(R.string.error_request, response.bodyAsText())) + } + } + } + + @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.BOOK_URL)) { + contentType(ContentType.Application.Json) + setBody(requestBody) + } + + when (response.status) { + 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)) + } + } + } + } 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/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt index 012fb6f..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 @@ -2,14 +2,12 @@ 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) } - } -} \ No newline at end of file + } \ 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/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/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 01b0f32..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 @@ -2,19 +2,20 @@ 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 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 @Composable fun AppNavHost( @@ -32,18 +33,16 @@ 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( + onBack = { navController.popBackStack() }, + onBookSuccess = { + // Возвращаемся на главный экран и обновляем его + navController.popBackStack() + } + ) } } } \ 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 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 f99978e..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 @@ -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 @@ -21,15 +22,18 @@ 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 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 @@ -38,13 +42,11 @@ fun AuthScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - viewModel.actionFlow.collect { - navController.navigate(MainScreenDestination) - } + viewModel.onIntent(AuthIntent.CheckLogIntent) } + Column( modifier = Modifier .fillMaxSize() @@ -58,12 +60,16 @@ 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) ) } + + is AuthState.LoggedIn -> { + navController.navigate(MainScreenDestination) + } } } } @@ -71,12 +77,36 @@ 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) } + var btnEnabled: Boolean by remember { mutableStateOf(false) } + + val event = viewModel.actionFlow.collectAsState(initial = null) + + // В 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.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), + modifier = Modifier + .testTag(TestIds.Auth.CODE_INPUT) + .fillMaxWidth(), value = inputText, onValueChange = { inputText = it @@ -86,12 +116,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..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 @@ -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 @@ -8,9 +10,14 @@ 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 +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 +25,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(replay = 1) + 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)) + _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 } + val authCode = authFlow().first() + if (authCode != "0") { + _actionFlow.emit(AuthAction.LogIn(true)) + _uiState.update { AuthState.LoggedIn } + } else { + _uiState.update { AuthState.Data } + } + + } + } } } } \ No newline at end of file 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..62744be --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.ui.screen.book + +import ru.myitschool.work.data.entity.Place +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: 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 new file mode 100644 index 0000000..461cb78 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -0,0 +1,180 @@ +package ru.myitschool.work.ui.screen.book + +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.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 ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.entity.Place +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Composable +fun BookScreen( + onBack: () -> Unit, + onBookSuccess: () -> Unit +) { + val viewModel: BookViewModel = viewModel() + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(viewModel.actionFlow) { + viewModel.actionFlow.collect { action -> + if (action is BookAction.BookSuccess) { + onBookSuccess() + } + } + } + + LaunchedEffect(Unit) { + viewModel.onIntent(BookIntent.LoadData) + } + + when (val state = uiState) { + is BookState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is BookState.Data -> { + BookContentScreen( + uiState = state, + 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: (Place) -> Unit, + onBook: () -> Unit, + 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), + ) { + availableDates.forEachIndexed { index, date -> + Tab( + selected = date == uiState.selectedDate, + onClick = { onSelectDate(date) }, + text = { + Text( + text = date.format(DateTimeFormatter.ofPattern("dd.MM")), + modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) + ) + }, + modifier = Modifier.testTag(TestIds.Book.ITEM_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(TestIds.Book.getIdPlaceItemByPosition(index)), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = place.place, + modifier = Modifier.weight(1f).testTag(TestIds.Book.ITEM_PLACE_TEXT) + ) + RadioButton( + selected = uiState.selectedPlace == place, + onClick = { onSelectPlace(place) }, + modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR) + ) + } + } + } + } + + if (availableDates.isEmpty() && !uiState.isError) { + Text( + text = "Всё забронировано", + modifier = Modifier.testTag(TestIds.Book.EMPTY) + ) + } + + if (uiState.isError) { + Text( + text = uiState.errorMessage ?: "Ошибка загрузки", + color = Color.Red, + modifier = Modifier.testTag(TestIds.Book.ERROR) + ) + + Button( + onClick = onRefresh, + modifier = Modifier.testTag(TestIds.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(TestIds.Book.BOOK_BUTTON) + ) { Text("Забронировать") } + } + + Button( + onClick = onBack, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag(TestIds.Book.BACK_BUTTON) + ) { + Text("Назад") + } + } +} \ 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..8163b48 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -0,0 +1,16 @@ +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 selectedDate: LocalDate? = null, + val selectedPlace: Place? = 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..b1b6e4f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -0,0 +1,150 @@ +package ru.myitschool.work.ui.screen.book + +import android.util.Log +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.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 +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() + + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + private var selectedPlaceId: Long? = null + + 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 } + + getAvailableBookingsUseCase().fold( + onSuccess = { bookings -> + if (bookings.isEmpty()) { + _uiState.update { + BookState.Data( + isError = true, + errorMessage = App.context.getString(R.string.error_no_available_dates) + ) + } + } 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 ?: App.context.getString(R.string.error_loading_data) + ) + } + } + ) + } + } + + private fun selectDate(date: LocalDate) { + _uiState.update { currentState -> + if (currentState is BookState.Data) { + currentState.copy( + selectedDate = date, + selectedPlace = null + ) + } else { + currentState + } + } + selectedPlaceId = null + } + + private fun selectPlace(place: Place) { + _uiState.update { currentState -> + if (currentState is BookState.Data) { + currentState.copy(selectedPlace = place) + } else { + currentState + } + } + selectedPlaceId = place.id + } + + private fun bookPlace() { + 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 (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() + _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))) + } + ) + } + } + } + + private fun refresh() { + loadBookData() + } +} 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 new file mode 100644 index 0000000..38da245 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainScreen.kt @@ -0,0 +1,244 @@ +package ru.myitschool.work.ui.screen.main + +import android.util.Log +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +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.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import coil3.compose.AsyncImage +import ru.myitschool.work.core.TestIds +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 + +@Composable +fun MainScreen( + navController: NavController, +) { + val viewModel: MainViewModel = viewModel() + // Состояния + val event = viewModel.actionFlow.collectAsState(initial = null) + // Функция загрузки данных + LaunchedEffect(Unit) { + 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 + } + } + // Если ошибка - показываем только ошибку и кнопку обновления + 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) } + + 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 + } + } + } + } + } + employee?.let { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // Верхняя строка + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Фото пользователя (main_photo) + AsyncImage( + model = employee?.photoUrl ?: "", + contentDescription = "Фото", + 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 = employee!!.name, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME), + color = MaterialTheme.colorScheme.onSurface + ) + + // Кнопка выхода (main_logout_button) + Button( + onClick = { + // Очистка данных и переход на авторизацию + viewModel.onIntent(MainIntent.LogOut) + bookingItems = emptyList() + }, + modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) + ) { + Text("Выход") + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Кнопки действий + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Кнопка обновления (main_refresh_button) + Button( + onClick = { viewModel.onIntent(MainIntent.LoadData) }, + enabled = state !is MainState.Loading, + modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) + ) { + if (state is MainState.Loading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Обновить") + } + } + // кнопка бронирования + Button( + onClick = { + navController.navigate(BookScreenDestination) + }, + modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) + ) { + Text("Перейти к бронированию") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Список бронирований + if (!bookingItems.isNullOrEmpty()) { + LazyColumn( + modifier = Modifier.weight(1f) + + ) { + itemsIndexed(bookingItems as List) { index, 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) + .testTag(TestIds.Main.getIdItemByPosition(index)) + ) { + // Дата бронирования (main_item_date) + Text( + text = "Дата: ${item?.date}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Место бронирования (main_item_place) + Text( + text = "Место: ${item?.place?.place}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) + ) + } + } + } + } + } else { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Нет бронирований", + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + } + } +} + + + + +@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..8a3f5bb --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -0,0 +1,61 @@ +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.main.GetUserDataUseCase + +class MainViewModel : ViewModel() { + 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() + } + } + } + } + + private 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())) + } + _uiState.update { MainState.Data(null) } + } + ) + } + } +} \ 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..874bb26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,25 @@ Привет! Введи код для авторизации Код Войти + Введён неверный код + Неправильный формат кода + + Ошибка запроса + Пустой ответ от сервера + Ошибка парсинга + В ответе отсутствует поле name + В ответе отсутствует поле photoUrl + В ответе отсутствует поле booking + В ответе отсутствует поле id + В ответе отсутствует поле place для даты + В ответе поле place пусто для даты + Список бронирований пуст + В информации о месте отсутствует id + В информации о месте отсутствует place + Ошибка бронирования + Ошибка бронирования + + Нет доступных дат для бронирования + Ошибка загрузки данных + \ No newline at end of file