diff --git a/app/src/main/java/ru/myitschool/work/data/models/Booking.kt b/app/src/main/java/ru/myitschool/work/data/models/Booking.kt new file mode 100644 index 0000000..a71aad5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/Booking.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Booking( + val id: Int, + val place: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/models/UserData.kt b/app/src/main/java/ru/myitschool/work/data/models/UserData.kt new file mode 100644 index 0000000..039837c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/models/UserData.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.data.models + +import kotlinx.serialization.Serializable + +@Serializable +data class UserData( + val name: String, + val photoUrl: String, + val booking: Map +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt new file mode 100644 index 0000000..ead988b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -0,0 +1,4 @@ +package ru.myitschool.work.data.repo + +object BookRepository { +} \ 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..cdfe93d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/MainRepository.kt @@ -0,0 +1,14 @@ +package ru.myitschool.work.data.repo + +import android.util.Log +import ru.myitschool.work.data.models.Booking +import ru.myitschool.work.data.models.UserData +import ru.myitschool.work.data.repo.AuthRepository.getCode +import ru.myitschool.work.data.source.LocalDataSource +import ru.myitschool.work.data.source.NetworkDataSource + +object MainRepository { + suspend fun fetch(): Result { + return NetworkDataSource.Info(getCode()).onSuccess { data -> data.name } + } +} \ 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..091ed82 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,6 +1,8 @@ package ru.myitschool.work.data.source +import android.util.Log import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.get @@ -11,6 +13,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +import ru.myitschool.work.data.models.Booking +import ru.myitschool.work.data.models.UserData object NetworkDataSource { private val client by lazy { @@ -39,4 +43,14 @@ object NetworkDataSource { } private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" + + suspend fun Info(code: String): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when (response.status) { + HttpStatusCode.OK -> Json.decodeFromString(response.body()) + else -> error(response.bodyAsText()) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt b/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt new file mode 100644 index 0000000..540e789 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/main/Fetch.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.domain.main + +import ru.myitschool.work.data.models.UserData +import ru.myitschool.work.data.repo.MainRepository + +class Fetch( + private val repository: MainRepository +) { + suspend operator fun invoke(): Result { + return repository.fetch().mapCatching { success -> success } + } +} \ 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 5aa3d78..96728ae 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 @@ -50,6 +50,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import coil3.compose.rememberAsyncImagePainter import ru.myitschool.work.core.TestIds +import ru.myitschool.work.data.models.Booking import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination @@ -59,51 +60,35 @@ fun MainScreen( navController: NavController ) { val state by viewModel.uiState.collectAsState() - - val bookings = listOf( - Booking(time = "08:00", place = "Meeting Room A"), - Booking(time = "09:30", place = "Conference Hall 1"), - Booking(time = "10:00", place = "CEO Office"), - Booking(time = "11:15", place = "Training Room B"), - Booking(time = "12:00", place = "Cafeteria (Lunch Meeting)"), - Booking(time = "13:30", place = "Boardroom"), - Booking(time = "14:00", place = "Design Studio"), - Booking(time = "15:00", place = "Meeting Room C"), - Booking(time = "16:15", place = "HR Office"), - Booking(time = "17:00", place = "Auditorium"), - Booking(time = "09:00", place = "Meeting Room B"), - Booking(time = "10:30", place = "Client Lounge"), - Booking(time = "11:00", place = "Server Room"), - Booking(time = "13:00", place = "Rooftop Terrace"), - Booking(time = "14:30", place = "Lab 3"), - Booking(time = "15:45", place = "Reception Area"), - Booking(time = "18:00", place = "Parking Lot (Team Event)"), - Booking(time = "19:30", place = "Offsite - Sky Bar"), - Booking(time = "08:30", place = "Video Conference Room"), - Booking(time = "16:30", place = "Gym (Wellness Session)") - ) + val err by viewModel.errorFlow.collectAsState() + val info by viewModel.infoFlow.collectAsState() when (val currentState = state) { is MainState.Error -> { - Text( - text = "TEST_ERROR", - modifier = Modifier.testTag(TestIds.Main.ERROR), - color = MaterialTheme.colorScheme.error - ) - IconButton( - onClick = { viewModel.onIntent(MainIntent.Fetch) }, - modifier = Modifier - .size(24.dp) - .aspectRatio(1f) - .testTag(TestIds.Main.REFRESH_BUTTON), - enabled = true, + Column ( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Icon( - Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier - .fillMaxSize() + Text( + text = err, + modifier = Modifier.testTag(TestIds.Main.ERROR), + color = MaterialTheme.colorScheme.error ) + IconButton( + onClick = { viewModel.onIntent(MainIntent.Fetch) }, + modifier = Modifier + .size(32.dp) + .aspectRatio(1f) + .testTag(TestIds.Main.REFRESH_BUTTON), + enabled = true, + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) + } } } @@ -117,132 +102,134 @@ fun MainScreen( } } is MainState.Data -> { - Column ( - modifier = Modifier.fillMaxSize() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), + info?.let { + Column ( + modifier = Modifier.fillMaxSize() ) { - FilledTonalIconButton( - onClick = { viewModel.onIntent(MainIntent.Logout) }, + Box( modifier = Modifier - .align(Alignment.TopEnd) - .size(40.dp) - .aspectRatio(1f) - .offset(x = -16.dp) - .testTag(TestIds.Main.LOGOUT_BUTTON), - enabled = true, - shape = MaterialTheme.shapes.extraLarge, - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) + .fillMaxWidth() + .padding(top = 20.dp), ) { - Icon( - Icons.AutoMirrored.Outlined.Logout, - contentDescription = null, + FilledTonalIconButton( + onClick = { viewModel.onIntent(MainIntent.Logout) }, modifier = Modifier - .size(20.dp) - ) - } - - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .size(120.dp) + .align(Alignment.TopEnd) + .size(40.dp) .aspectRatio(1f) - .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) - ) { - Image( - painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .size(105.dp) - .clip(CircleShape) - .testTag(TestIds.Main.PROFILE_IMAGE) + .offset(x = -16.dp) + .testTag(TestIds.Main.LOGOUT_BUTTON), + enabled = true, + shape = MaterialTheme.shapes.extraLarge, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer ) - } - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "Smirnova Anna", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .testTag(TestIds.Main.PROFILE_NAME) - ) - Spacer(modifier = Modifier.size(16.dp)) - } - } - - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerLow, RoundedCornerShape(topEnd = 24.dp , topStart = 24.dp)) - ) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier - .padding(vertical = 16.dp, horizontal = 16.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Absolute.SpaceBetween ) { Icon( - imageVector = Icons.Default.BookmarkBorder, + Icons.AutoMirrored.Outlined.Logout, contentDescription = null, - ) - Text( - text = "Бронирования", - style = MaterialTheme.typography.titleMedium, - ) - IconButton( - onClick = { viewModel.onIntent(MainIntent.Fetch) }, modifier = Modifier - .size(24.dp) + .size(20.dp) + ) + } + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .size(120.dp) .aspectRatio(1f) - .testTag(TestIds.Main.REFRESH_BUTTON), - enabled = true, + .background(MaterialTheme.colorScheme.inverseOnSurface, CircleShape) ) { - Icon( - Icons.Default.Refresh, + Image( + painter = rememberAsyncImagePainter("https://catalog-cdn.detmir.st/media/2fe02057f9915e72a378795d32c79ea9.jpeg"), contentDescription = null, + contentScale = ContentScale.Fit, modifier = Modifier - .fillMaxSize() + .size(105.dp) + .clip(CircleShape) + .testTag(TestIds.Main.PROFILE_IMAGE) ) } - } - HorizontalDivider( - color = MaterialTheme.colorScheme.outlineVariant, - thickness = 1.dp, - ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - ) { - itemsIndexed(bookings) { index, booking -> - Booking(booking = booking, index = index) - } + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "Smirnova Anna", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .testTag(TestIds.Main.PROFILE_NAME) + ) + Spacer(modifier = Modifier.size(16.dp)) } } - FloatingActionButton( - onClick = { viewModel.onIntent(MainIntent.NewBooking) }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + Box( modifier = Modifier - .align(Alignment.BottomEnd) - .offset(x = -16.dp, y = -16.dp) - .testTag(TestIds.Main.ADD_BUTTON) + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLow, RoundedCornerShape(topEnd = 24.dp , topStart = 24.dp)) ) { - Icon(Icons.Default.Add, contentDescription = "Добавить") + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + Icon( + imageVector = Icons.Default.BookmarkBorder, + contentDescription = null, + ) + Text( + text = "Бронирования", + style = MaterialTheme.typography.titleMedium, + ) + IconButton( + onClick = { viewModel.onIntent(MainIntent.Fetch) }, + modifier = Modifier + .size(24.dp) + .aspectRatio(1f) + .testTag(TestIds.Main.REFRESH_BUTTON), + enabled = true, + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + ) + } + } + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 1.dp, + ) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + itemsIndexed(info!!.booking.entries.toList()) { index, booking -> + Booking(booking = booking.value, date = booking.key, index = index) + } + } + } + + FloatingActionButton( + onClick = { viewModel.onIntent(MainIntent.NewBooking) }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .align(Alignment.BottomEnd) + .offset(x = -16.dp, y = -16.dp) + .testTag(TestIds.Main.ADD_BUTTON) + ) { + Icon(Icons.Default.Add, contentDescription = "Добавить") + } } } } @@ -259,7 +246,7 @@ fun MainScreen( } @Composable -private fun Booking(booking: Booking, index: Int){ +private fun Booking(booking: Booking, date: String, index: Int){ Row( modifier = Modifier .fillMaxWidth() @@ -270,7 +257,7 @@ private fun Booking(booking: Booking, index: Int){ ) { Column(modifier = Modifier.weight(1f)) { Text( - text = booking.time, + text = date, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) @@ -295,9 +282,4 @@ private fun Booking(booking: Booking, index: Int){ thickness = 1.dp, modifier = Modifier.padding(start = 16.dp) ) -} - -data class Booking( - val time: String, - val place: String -) \ No newline at end of file +} \ 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 index 41c0e3f..ec65c7d 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.main +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -7,25 +8,49 @@ 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.models.UserData import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.data.repo.MainRepository +import ru.myitschool.work.domain.main.Fetch import ru.myitschool.work.domain.main.Logout class MainViewModel(): ViewModel() { + private val fetch by lazy { Fetch(MainRepository) } private val logout by lazy { Logout(AuthRepository) } - private val _uiState = MutableStateFlow(MainState.Data) + private val _uiState = MutableStateFlow(MainState.Loading) val uiState: StateFlow = _uiState.asStateFlow() private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + private val _infoFlow: MutableStateFlow = MutableStateFlow(null) + val infoFlow: StateFlow = _infoFlow.asStateFlow() + private val _errorFlow = MutableStateFlow("") + val errorFlow: StateFlow = _errorFlow.asStateFlow() fun onIntent(intent: MainIntent) { when (intent) { - is MainIntent.Fetch -> Unit + is MainIntent.Fetch -> viewModelScope.launch { + _uiState.update { MainState.Loading } + fetch.invoke().fold( + onSuccess = { success -> + _infoFlow.update { success } + _uiState.update { MainState.Data } + }, + onFailure = { failure -> + Log.d(failure.message, "") + _uiState.update { MainState.Error } + _errorFlow.update { failure.message.toString() } + } + ) + } + is MainIntent.Logout -> { viewModelScope.launch { logout.invoke() } } + is MainIntent.NewBooking -> { viewModelScope.launch { _actionFlow.emit(Unit)