add full logic to Auth & Main

This commit is contained in:
2025-11-30 18:39:02 +03:00
parent 48f4f9fd6f
commit 4e45459af2
30 changed files with 785 additions and 156 deletions

View File

@@ -15,6 +15,12 @@ class DataStoreManager(
private val USER_CODE_KEY = stringPreferencesKey("user_code")
}
suspend fun clearUserCode() {
dataStore.edit { preferences ->
preferences.remove(USER_CODE_KEY)
}
}
suspend fun saveUserCode(userCode: UserCode) {
dataStore.edit { preferences ->
preferences[USER_CODE_KEY] = userCode.code

View File

@@ -4,13 +4,8 @@ import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository {
private var codeCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
}
}
return NetworkDataSource.checkAuth(text)
}
}

View File

@@ -0,0 +1,12 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.domain.main.entities.UserEntity
object BookRepository {
suspend fun loadBooking(text: String): Result<BookingEntity> {
return NetworkDataSource.loadBooking(text)
}
}

View File

@@ -0,0 +1,11 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.main.entities.UserEntity
object MainRepository {
suspend fun loadData(text: String): Result<UserEntity> {
return NetworkDataSource.loadData(text)
}
}

View File

@@ -1,6 +1,7 @@
package ru.myitschool.work.data.source
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 +12,30 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.domain.book.entities.PlaceInfo
import ru.myitschool.work.domain.main.entities.UserEntity
private const val testJson = """
{
"name": "Иванов Петр Федорович",
"photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
"booking": {
"2025-01-05": {"id":1,"place":"102"},
"2025-01-06": {"id":2,"place":"209.13"},
"2025-01-09": {"id":3,"place":"Зона 51. 50"}
}
}
"""
private const val testBookingJson = """
{
"2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-06": [{"id": 3, "place": "Зона 51. 50"}],
"2025-01-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-08": [{"id": 2, "place": "209.13"}]
}
"""
object NetworkDataSource {
private val client by lazy {
@@ -30,12 +55,46 @@ object NetworkDataSource {
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
true // удалить при проверке
// val response = client.get(getUrl(code, Constants.AUTH_URL))
// response.status
// when (response.status) {
// HttpStatusCode.OK -> true
// else -> error(response.bodyAsText())
// }
}
}
suspend fun loadData(code: String): Result<UserEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching {
Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
// val response = client.get(getUrl(code, Constants.INFO_URL))
// when (response.status) {
// HttpStatusCode.OK -> {
// response.body<UserEntity>()
// }
// else -> error(response.bodyAsText())
// }
}
}
suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching {
BookingEntity(Json.decodeFromString<Map<String, List<PlaceInfo>>>(testBookingJson)) // удалить при проверке
// val response = client.get(getUrl(code, Constants.BOOKING_URL))
// when (response.status) {
// HttpStatusCode.OK -> {
// BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
// }
// else -> error(response.bodyAsText())
// }
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"

View File

@@ -0,0 +1,16 @@
package ru.myitschool.work.domain.book
import ru.myitschool.work.data.repo.BookRepository
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadBookingUseCase(
private val repository: BookRepository
) {
suspend operator fun invoke(
text: String
): Result<BookingEntity> {
return repository.loadBooking(text)
}
}

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.domain.book.entities
import kotlinx.serialization.Serializable
@Serializable
data class BookingEntity(
val bookings: Map<String, List<PlaceInfo>>
)

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.domain.book.entities
import kotlinx.serialization.Serializable
@Serializable
data class PlaceInfo(
val id: Int,
val place: String
)

View File

@@ -0,0 +1,14 @@
package ru.myitschool.work.domain.main
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadDataUseCase(
private val repository: MainRepository
) {
suspend operator fun invoke(
text: String
): Result<UserEntity> {
return repository.loadData(text)
}
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.domain.main.entities
import kotlinx.serialization.Serializable
@Serializable
data class BookingInfo(
val id: Int,
val place: String
)

View File

@@ -0,0 +1,26 @@
package ru.myitschool.work.domain.main.entities
import kotlinx.serialization.Serializable
import ru.myitschool.work.formatDate
@Serializable
data class UserEntity(
val name: String,
val photoUrl: String,
val booking: Map<String, BookingInfo>? = null
) {
fun getSortedBookings(): List<Pair<String, BookingInfo>> {
return booking?.entries
?.sortedBy { (date, _) -> date }
?.map { it.toPair() }
?: emptyList()
}
fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>> {
return getSortedBookings().map { (date, bookingInfo) ->
Triple(date, date.formatDate(), bookingInfo)
}
}
fun hasBookings(): Boolean = !booking.isNullOrEmpty()
}

View File

@@ -1,13 +1,10 @@
package ru.myitschool.work.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -15,23 +12,15 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds.Main
import ru.myitschool.work.ui.theme.Black
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.Gray
import ru.myitschool.work.ui.theme.LightBlue
import ru.myitschool.work.ui.theme.LightGray
@@ -181,6 +170,7 @@ fun BaseText20(
@Composable
fun BaseButton(
border: BorderStroke? = null,
enable: Boolean = true,
text: String,
btnColor: Color,
@@ -190,6 +180,7 @@ fun BaseButton(
modifier: Modifier = Modifier.fillMaxWidth()
) {
Button(
border = border,
enabled = enable,
onClick = onClick,
colors = ButtonDefaults.buttonColors(
@@ -205,32 +196,3 @@ fun BaseButton(
BaseText20(text = text)
}
}
@Composable
fun ErrorScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 40.dp)
) {
Spacer(modifier = Modifier.height(80.dp))
BaseText24(
text = "Ошибка загрузки данных",
textAlign = TextAlign.Center,
modifier = Modifier
.testTag(Main.ERROR)
.width(250.dp)
)
Spacer(modifier = Modifier.height(30.dp))
BaseButton(
modifier = Modifier.testTag(Main.REFRESH_BUTTON),
text = "Обновить",
btnColor = Blue,
btnContentColor = White,
onClick = {}
)
}
}

View File

@@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object AuthScreenDestination: ru.myitschool.work.ui.nav.AppDestination
data object AuthScreenDestination: AppDestination

View File

@@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object BookScreenDestination: ru.myitschool.work.ui.nav.AppDestination
data object BookScreenDestination: AppDestination

View File

@@ -3,6 +3,4 @@ package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object MainScreenDestination: AppDestination {
val userData = ""
}
data object MainScreenDestination: AppDestination

View File

@@ -16,6 +16,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.nav.SplashScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
import ru.myitschool.work.ui.screen.splash.SplashScreen
@@ -41,11 +42,7 @@ fun AppNavHost(
MainScreen(navController = navController)
}
composable<BookScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
BookScreen(navController = navController)
}
}
}

View File

@@ -36,7 +36,7 @@ class AuthViewModel(application: Application) : AndroidViewModel(application) {
fun onIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
onSuccess = {

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookAction {
object Auth: BookAction
object Main: BookAction
}

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookIntent {
object Back: BookIntent
object LoadBooking: BookIntent
}

View File

@@ -2,89 +2,266 @@ package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.core.TestIds.Book
import ru.myitschool.work.core.TestIds.Main
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.formatBookingDate
import ru.myitschool.work.formatDate
import ru.myitschool.work.ui.BaseButton
import ru.myitschool.work.ui.BaseNoBackgroundButton
import ru.myitschool.work.ui.BaseText16
import ru.myitschool.work.ui.BaseText20
import ru.myitschool.work.ui.BaseText24
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.main.MainIntent
import ru.myitschool.work.ui.theme.Black
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.Gray
import ru.myitschool.work.ui.theme.LightGray
import ru.myitschool.work.ui.theme.MontserratFontFamily
import ru.myitschool.work.ui.theme.Typography
import ru.myitschool.work.ui.theme.White
@Composable
fun BookScreen(
navController: NavController
navController: NavController,
viewModel: BookViewModel = viewModel(),
) {
Column(
val state by viewModel.uiState.collectAsState()
) {
Row(
LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action ->
when(action) {
is BookAction.Auth -> navController.navigate(AuthScreenDestination)
is BookAction.Main -> navController.navigate(MainScreenDestination)
}
}
}
when(state) {
is BookState.Loading -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
BaseText24(
text = "Новая встреча"
)
BaseNoBackgroundButton(
text = "Назад",
onClick = {}
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
}
Column(
}
is BookState.Data -> {
DataContent(
viewModel,
bookingData = (state as? BookState.Data)?.userBooking
)
}
is BookState.Error -> ErrorContent(viewModel)
is BookState.Empty -> EmptyContent(viewModel)
}
}
@Composable
fun EmptyContent(
viewModel: BookViewModel
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
BaseText16(
text = "Доступные даты"
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(15.dp)
.fillMaxHeight()
.width(320.dp)
) {
Spacer(modifier = Modifier.height(80.dp))
BaseText24(
text = stringResource(R.string.book_all_booked),
modifier = Modifier.testTag(Book.EMPTY),
textAlign = TextAlign.Center
)
BookDateList()
Spacer(modifier = Modifier.height(20.dp))
BaseText16(
text = "Выберите место встречи"
BaseButton(
text = stringResource(R.string.book_back),
modifier = Modifier
.fillMaxWidth()
.testTag(Book.BACK_BUTTON),
onClick = { viewModel.onIntent(BookIntent.Back) },
btnContentColor = White,
btnColor = Blue
)
}
}
}
@Composable
fun ErrorContent(
viewModel: BookViewModel
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(15.dp)
.fillMaxHeight()
.width(320.dp)
) {
Spacer(modifier = Modifier.height(80.dp))
BaseText24(
text = stringResource(R.string.book_error),
modifier = Modifier.testTag(Book.ERROR),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(20.dp))
BaseButton(
border = BorderStroke(1.dp, Blue),
text = stringResource(R.string.book_back),
modifier = Modifier
.fillMaxWidth()
.testTag(Book.BACK_BUTTON),
onClick = { viewModel.onIntent(BookIntent.Back) },
btnContentColor = Blue,
btnColor = Color.Transparent
)
Spacer(modifier = Modifier.height(15.dp))
BaseButton(
text = stringResource(R.string.main_update),
modifier = Modifier
.fillMaxWidth()
.testTag(Book.REFRESH_BUTTON),
onClick = { viewModel.onIntent(BookIntent.LoadBooking) },
btnContentColor = White,
btnColor = Blue
)
}
}
}
@Composable
fun DataContent(
viewModel: BookViewModel,
bookingData: BookingEntity?
) {
Column {
Row(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
.background(Blue)
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
BaseText24(
text = stringResource(R.string.book_new_book),
color = White,
modifier = Modifier.padding(start = 15.dp)
)
BaseNoBackgroundButton(
text = stringResource(R.string.book_back),
modifier = Modifier.testTag(Book.BACK_BUTTON),
onClick = { viewModel.onIntent(BookIntent.Back) }
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 20.dp, horizontal = 10.dp)
.clip(RoundedCornerShape(16.dp))
.background(White)
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(13.dp)
) {
Column {
Text(
text = stringResource(R.string.book_available_date),
style = Typography.bodyMedium,
fontSize = 16.sp,
)
BookDateList(bookingData?.bookings?.keys?.toList() ?: emptyList())
Text(
text = stringResource(R.string.book_choose_place),
style = Typography.bodyMedium,
fontSize = 16.sp,
)
BookPlaceList()
}
BaseButton(
text = "Бронировать",
text = stringResource(R.string.booking_button),
btnColor = Blue,
btnContentColor = White,
onClick = { navController.navigate(BookScreenDestination)},
onClick = { },
modifier = Modifier
.testTag(Main.ADD_BUTTON)
.padding(horizontal = 10.dp, vertical = 15.dp)
.testTag(Book.BOOK_BUTTON)
.padding(horizontal = 10.dp)
.fillMaxWidth(),
icon = {Image(
icon = { Image(
painter = painterResource(R.drawable.add_icon),
contentDescription = "plus Icon"
)}
contentDescription = stringResource(R.string.add_icon_description)
) }
)
}
}
}
}
@Composable
@@ -98,32 +275,36 @@ fun BookPlaceListElement() {
}
@Composable
fun BookDateList() {
BookDateListElement()
BookDateListElement()
BookDateListElement()
BookDateListElement()
BookDateListElement()
BookDateListElement()
BookDateListElement()
fun BookDateList(dates: List<String>) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier.padding(vertical = 15.dp)
) {
dates.forEach { date ->
BookDateListElement(date = date, onClick = {
// Обработка выбора даты
})
}
}
}
@Composable
fun BookDateListElement() {
fun BookDateListElement(date: String, onClick: () -> Unit) {
Button(
border = BorderStroke(1.dp, Black),
onClick = {},
contentPadding = PaddingValues(0.dp),
modifier = Modifier
.width(60.dp)
.testTag(Book.ITEM_DATE)
.padding(0.dp),
border = BorderStroke(1.dp, Black),
onClick = onClick,
colors = ButtonColors(
contentColor = Black,
containerColor = Color.Transparent,
disabledContentColor = Black,
disabledContainerColor = Color.Transparent),
shape = RoundedCornerShape(16.dp)
) {
BaseText16(text = "16.06")
val formattedDate = formatBookingDate(date)
BaseText16(text = formattedDate)
}
}

View File

@@ -0,0 +1,10 @@
package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.domain.book.entities.BookingEntity
sealed interface BookState {
object Loading: BookState
data class Data(val userBooking: BookingEntity): BookState
object Error: BookState
object Empty: BookState
}

View File

@@ -0,0 +1,83 @@
package ru.myitschool.work.ui.screen.book
import android.app.Application
import androidx.lifecycle.AndroidViewModel
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.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.data.repo.BookRepository
import ru.myitschool.work.domain.book.LoadBookingUseCase
import ru.myitschool.work.ui.screen.main.MainAction
import ru.myitschool.work.ui.screen.main.MainIntent
import kotlin.text.isEmpty
class BookViewModel(application: Application) : AndroidViewModel(application) {
private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) }
private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager
}
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<BookAction> = MutableSharedFlow()
val actionFlow: SharedFlow<BookAction> = _actionFlow
init {
loadBooking()
}
private fun loadBooking() {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { BookState.Loading }
try {
val userCode = dataStoreManager.getUserCode().first()
if (userCode.code.isEmpty()) {
_actionFlow.emit(BookAction.Auth)
return@launch
}
loadBookingUseCase.invoke(userCode.code).fold(
onSuccess = { data ->
if (data.bookings.isEmpty()) {
_uiState.update { BookState.Empty }
}
else {
_uiState.update { BookState.Data(data) }
}
},
onFailure = { error ->
error.printStackTrace()
_uiState.update { BookState.Error }
}
)
} catch (error: Exception) {
error.printStackTrace()
_uiState.update { BookState.Error }
}
}
}
fun onIntent( intent: BookIntent) {
when(intent) {
is BookIntent.LoadBooking -> loadBooking()
is BookIntent.Back -> {
viewModelScope.launch(Dispatchers.Default) {
_actionFlow.emit(BookAction.Main)
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainAction {
object Booking: MainAction
object Auth: MainAction
}

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainIntent {
object Logout: MainIntent
object Booking: MainIntent
object LoadData: MainIntent
}

View File

@@ -7,34 +7,47 @@ 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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
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.runtime.remember
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil3.compose.rememberAsyncImagePainter
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds.Main
import ru.myitschool.work.domain.main.entities.BookingInfo
import ru.myitschool.work.domain.main.entities.UserEntity
import ru.myitschool.work.ui.BaseButton
import ru.myitschool.work.ui.BaseNoBackgroundButton
import ru.myitschool.work.ui.BaseText14
import ru.myitschool.work.ui.BaseText16
import ru.myitschool.work.ui.BaseText20
import ru.myitschool.work.ui.BaseText24
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.theme.Black
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.LightGray
@@ -49,8 +62,19 @@ fun MainScreen(
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action ->
when(action) {
is MainAction.Auth -> navController.navigate(AuthScreenDestination)
is MainAction.Booking -> navController.navigate(BookScreenDestination)
}
}
}
when(state) {
is MainState.Loading -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
@@ -61,16 +85,59 @@ fun MainScreen(
}
}
is MainState.Error -> {
ErrorContent(viewModel)
}
is MainState.Data -> {
Content(viewModel)
DataContent(
viewModel,
userData = (state as MainState.Data).userData
)
}
}
}
@Composable
fun Content(viewModel: MainViewModel) {
fun ErrorContent(viewModel: MainViewModel){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(15.dp)
.fillMaxHeight()
.width(320.dp)
) {
Spacer(modifier = Modifier.height(80.dp))
BaseText24(
text = stringResource(R.string.data_error_message),
modifier = Modifier.testTag(Main.ERROR),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(20.dp))
BaseButton(
text = stringResource(R.string.main_update),
modifier = Modifier
.fillMaxWidth()
.testTag(Main.REFRESH_BUTTON),
onClick = { viewModel.onIntent(MainIntent.LoadData) },
btnContentColor = White,
btnColor = Blue
)
}
}
}
@Composable
fun DataContent(
viewModel: MainViewModel,
userData: UserEntity
) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
@@ -92,27 +159,33 @@ fun Content(viewModel: MainViewModel) {
modifier = Modifier.fillMaxWidth()
) {
BaseNoBackgroundButton(
text = "Обновить",
onClick = { },
text = stringResource(R.string.main_update),
onClick = { viewModel.onIntent(MainIntent.LoadData) },
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
)
BaseNoBackgroundButton(
text = "Выйти",
onClick = { },
text = stringResource(R.string.main_log_out),
onClick = { viewModel.onIntent(MainIntent.Logout) },
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
)
}
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "User avatar",
painter = rememberAsyncImagePainter(
model = userData.photoUrl,
error = painterResource(R.drawable.avatar)
),
contentDescription = stringResource(R.string.main_avatar_description),
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.testTag(Main.PROFILE_IMAGE)
.width(150.dp)
.height(150.dp)
.padding(20.dp)
)
BaseText20(
text = "Артемий Артемиев Иванович",
text = userData.name,
color = White,
textAlign = TextAlign.Center,
modifier = Modifier
@@ -135,7 +208,7 @@ fun Content(viewModel: MainViewModel) {
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Ваши забронированные места",
text = stringResource(R.string.main_booking_title),
style = Typography.bodyMedium,
color = Black,
fontSize = 16.sp,
@@ -144,20 +217,24 @@ fun Content(viewModel: MainViewModel) {
vertical = 20.dp
)
)
BookList()
if (userData.hasBookings()) {
SortedBookingList(userData = userData)
} else {
EmptyBookings()
}
}
BaseButton(
text = "Бронировать",
text = stringResource(R.string.booking_button),
btnColor = Blue,
btnContentColor = White,
onClick = {},
onClick = { viewModel.onIntent(MainIntent.Booking) },
modifier = Modifier
.testTag(Main.ADD_BUTTON)
.padding(horizontal = 10.dp, vertical = 15.dp)
.fillMaxWidth(),
icon = {Image(
painter = painterResource(R.drawable.add_icon),
contentDescription = "plus Icon"
contentDescription = stringResource(R.string.add_icon_description)
)}
)
@@ -165,25 +242,38 @@ fun Content(viewModel: MainViewModel) {
}
}
val bookListData = listOf(
"Конгресс Холл",
"Конгресс Холл",
"Конгресс Холл"
)
@Composable
fun BookList() {
Column(
modifier = Modifier.padding(horizontal = 20.dp)
fun SortedBookingList(userData: UserEntity) {
val sortedBookings = remember(userData.booking) {
userData.getSortedBookingsWithFormattedDate()
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
for ((index, book) in bookListData.withIndex()) {
BookListElement(index)
itemsIndexed(
items = sortedBookings
) { index, (originalDate, formattedDate, bookingInfo) ->
BookingItem(
originalDate = originalDate,
formattedDate = formattedDate,
bookingInfo = bookingInfo,
index = index
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Composable
fun BookListElement(index: Int) {
fun BookingItem(
originalDate: String,
formattedDate: String,
bookingInfo: BookingInfo,
index: Int
) {
Row(
modifier = Modifier
.testTag(Main.getIdItemByPosition(index))
@@ -192,12 +282,26 @@ fun BookListElement(index: Int) {
horizontalArrangement = Arrangement.SpaceBetween
) {
BaseText14(
text = "Конгресс Холл",
text = bookingInfo.place,
modifier = Modifier.testTag(Main.ITEM_PLACE)
)
BaseText14(
text = "16.02.3026",
text = formattedDate,
modifier = Modifier.testTag(Main.ITEM_DATE)
)
}
}
@Composable
fun EmptyBookings() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
BaseText16(
text = stringResource(R.string.main_empty_booking)
)
}
}

View File

@@ -1,7 +1,9 @@
package ru.myitschool.work.ui.screen.main
import ru.myitschool.work.domain.main.entities.UserEntity
sealed interface MainState {
object Data: MainState
data class Data(val userData: UserEntity): MainState
object Loading: MainState
object Error: MainState
}

View File

@@ -1,15 +1,82 @@
package ru.myitschool.work.ui.screen.main
import androidx.lifecycle.ViewModel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
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.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.LoadDataUseCase
class MainViewModel: ViewModel() {
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager
}
private val loadDataUseCase by lazy { LoadDataUseCase(MainRepository) }
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
val actionFlow: SharedFlow<MainAction> = _actionFlow
init {
loadData()
}
private fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { MainState.Loading }
try {
val userCode = dataStoreManager.getUserCode().first()
if (userCode.code.isEmpty()) {
_actionFlow.emit(MainAction.Auth)
return@launch
}
loadDataUseCase.invoke(userCode.code).fold(
onSuccess = { data ->
_uiState.update { MainState.Data(data) }
},
onFailure = { error ->
error.printStackTrace()
_uiState.update { MainState.Error }
}
)
} catch (error: Exception) {
error.printStackTrace()
_uiState.update { MainState.Error }
}
}
}
fun onIntent( intent: MainIntent) {
when(intent) {
is MainIntent.LoadData -> loadData()
is MainIntent.Booking -> {
viewModelScope.launch(Dispatchers.Default) {
_actionFlow.emit(MainAction.Booking)
}
}
is MainIntent.Logout -> {
viewModelScope.launch(Dispatchers.IO) {
dataStoreManager.clearUserCode()
_actionFlow.emit(MainAction.Auth)
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
package ru.myitschool.work.ui.screen.splash
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
@@ -28,12 +29,7 @@ class SplashViewModel(application: Application) : AndroidViewModel(application)
try {
val userCode = dataStoreManager.getUserCode().first()
val isAuthenticated = when {
userCode == null -> false
userCode.code is String -> (userCode.code as String).isNotEmpty()
userCode.code is Int -> (userCode.code as Int) != -1
else -> false
}
val isAuthenticated = if (userCode.code.isEmpty()) false else true
_splashState.value = if (isAuthenticated) {
SplashState.Authenticated

View File

@@ -0,0 +1,25 @@
package ru.myitschool.work
fun String.formatDate(): String {
return try {
val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
val outputFormat = java.text.SimpleDateFormat("dd.MM.yyyy", java.util.Locale.getDefault())
val date = inputFormat.parse(this)
outputFormat.format(date)
} catch (e: Exception) {
this
}
}
fun formatBookingDate(dateString: String): String {
return try {
val parts = dateString.split("-")
if (parts.size == 3) {
"${parts[2]}.${parts[1]}"
} else {
dateString
}
} catch (e: Exception) {
dateString
}
}

View File

@@ -4,4 +4,18 @@
<string name="auth_title">Введите код для авторизации</string>
<string name="auth_label">Код</string>
<string name="auth_sign_in">Войти</string>
<string name="main_update">Обновить</string>
<string name="main_log_out">Выйти</string>
<string name="main_avatar_description">Фото пользователя</string>
<string name="main_booking_title">Ваши забронированные места</string>
<string name="booking_button">Бронировать</string>
<string name="add_icon_description">Иконка добавления</string>
<string name="data_error_message">Ошибка загрузки данных</string>
<string name="main_empty_booking">Нет бронирований</string>
<string name="book_new_book">Новая встреча</string>
<string name="book_back">Назад</string>
<string name="book_available_date">Доступные даты</string>
<string name="book_choose_place">Выберите место встречи</string>
<string name="book_all_booked">Всё забронировано</string>
<string name="book_error">Ошибка сервера</string>
</resources>