forked from Olympic/NTO-2025-Android-TeamTask
add full logic to Auth & Main
This commit is contained in:
@@ -15,6 +15,12 @@ class DataStoreManager(
|
|||||||
private val USER_CODE_KEY = stringPreferencesKey("user_code")
|
private val USER_CODE_KEY = stringPreferencesKey("user_code")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearUserCode() {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
preferences.remove(USER_CODE_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun saveUserCode(userCode: UserCode) {
|
suspend fun saveUserCode(userCode: UserCode) {
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[USER_CODE_KEY] = userCode.code
|
preferences[USER_CODE_KEY] = userCode.code
|
||||||
|
|||||||
@@ -4,13 +4,8 @@ import ru.myitschool.work.data.source.NetworkDataSource
|
|||||||
|
|
||||||
object AuthRepository {
|
object AuthRepository {
|
||||||
|
|
||||||
private var codeCache: String? = null
|
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
return NetworkDataSource.checkAuth(text)
|
||||||
if (success) {
|
|
||||||
codeCache = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
@@ -11,6 +12,30 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.myitschool.work.core.Constants
|
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 {
|
object NetworkDataSource {
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
@@ -30,11 +55,45 @@ object NetworkDataSource {
|
|||||||
|
|
||||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
|
||||||
when (response.status) {
|
true // удалить при проверке
|
||||||
HttpStatusCode.OK -> true
|
|
||||||
else -> error(response.bodyAsText())
|
// 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())
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>>
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package ru.myitschool.work.ui
|
package ru.myitschool.work.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -15,23 +12,15 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ru.myitschool.work.R
|
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.Black
|
||||||
import ru.myitschool.work.ui.theme.Blue
|
|
||||||
import ru.myitschool.work.ui.theme.Gray
|
import ru.myitschool.work.ui.theme.Gray
|
||||||
import ru.myitschool.work.ui.theme.LightBlue
|
import ru.myitschool.work.ui.theme.LightBlue
|
||||||
import ru.myitschool.work.ui.theme.LightGray
|
import ru.myitschool.work.ui.theme.LightGray
|
||||||
@@ -181,6 +170,7 @@ fun BaseText20(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseButton(
|
fun BaseButton(
|
||||||
|
border: BorderStroke? = null,
|
||||||
enable: Boolean = true,
|
enable: Boolean = true,
|
||||||
text: String,
|
text: String,
|
||||||
btnColor: Color,
|
btnColor: Color,
|
||||||
@@ -190,6 +180,7 @@ fun BaseButton(
|
|||||||
modifier: Modifier = Modifier.fillMaxWidth()
|
modifier: Modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
|
border = border,
|
||||||
enabled = enable,
|
enabled = enable,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -204,33 +195,4 @@ fun BaseButton(
|
|||||||
icon()
|
icon()
|
||||||
BaseText20(text = text)
|
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 = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object AuthScreenDestination: ru.myitschool.work.ui.nav.AppDestination
|
data object AuthScreenDestination: AppDestination
|
||||||
@@ -3,4 +3,4 @@ package ru.myitschool.work.ui.nav
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object BookScreenDestination: ru.myitschool.work.ui.nav.AppDestination
|
data object BookScreenDestination: AppDestination
|
||||||
@@ -3,6 +3,4 @@ package ru.myitschool.work.ui.nav
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object MainScreenDestination: AppDestination {
|
data object MainScreenDestination: AppDestination
|
||||||
val userData = ""
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import ru.myitschool.work.ui.nav.BookScreenDestination
|
|||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
import ru.myitschool.work.ui.nav.SplashScreenDestination
|
import ru.myitschool.work.ui.nav.SplashScreenDestination
|
||||||
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
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.main.MainScreen
|
||||||
import ru.myitschool.work.ui.screen.splash.SplashScreen
|
import ru.myitschool.work.ui.screen.splash.SplashScreen
|
||||||
|
|
||||||
@@ -41,11 +42,7 @@ fun AppNavHost(
|
|||||||
MainScreen(navController = navController)
|
MainScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
Box(
|
BookScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ class AuthViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_uiState.update { AuthState.Loading }
|
_uiState.update { AuthState.Loading }
|
||||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookAction {
|
||||||
|
object Auth: BookAction
|
||||||
|
object Main: BookAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookIntent {
|
||||||
|
object Back: BookIntent
|
||||||
|
object LoadBooking: BookIntent
|
||||||
|
}
|
||||||
@@ -2,91 +2,268 @@ package ru.myitschool.work.ui.screen.book
|
|||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonColors
|
import androidx.compose.material3.ButtonColors
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
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.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import ru.myitschool.work.R
|
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.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.BaseButton
|
||||||
import ru.myitschool.work.ui.BaseNoBackgroundButton
|
import ru.myitschool.work.ui.BaseNoBackgroundButton
|
||||||
import ru.myitschool.work.ui.BaseText16
|
import ru.myitschool.work.ui.BaseText16
|
||||||
import ru.myitschool.work.ui.BaseText20
|
|
||||||
import ru.myitschool.work.ui.BaseText24
|
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.Black
|
||||||
import ru.myitschool.work.ui.theme.Blue
|
import ru.myitschool.work.ui.theme.Blue
|
||||||
import ru.myitschool.work.ui.theme.Gray
|
import ru.myitschool.work.ui.theme.Typography
|
||||||
import ru.myitschool.work.ui.theme.LightGray
|
|
||||||
import ru.myitschool.work.ui.theme.MontserratFontFamily
|
|
||||||
import ru.myitschool.work.ui.theme.White
|
import ru.myitschool.work.ui.theme.White
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookScreen(
|
fun BookScreen(
|
||||||
navController: NavController
|
navController: NavController,
|
||||||
|
viewModel: BookViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
Column(
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
) {
|
LaunchedEffect(Unit) {
|
||||||
Row(
|
viewModel.actionFlow.collect { action ->
|
||||||
|
when(action) {
|
||||||
|
is BookAction.Auth -> navController.navigate(AuthScreenDestination)
|
||||||
|
|
||||||
) {
|
is BookAction.Main -> navController.navigate(MainScreenDestination)
|
||||||
BaseText24(
|
}
|
||||||
text = "Новая встреча"
|
}
|
||||||
)
|
}
|
||||||
BaseNoBackgroundButton(
|
|
||||||
text = "Назад",
|
when(state) {
|
||||||
onClick = {}
|
is BookState.Loading -> {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(320.dp)
|
||||||
) {
|
) {
|
||||||
BaseText16(
|
|
||||||
text = "Доступные даты"
|
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 = "Выберите место встречи"
|
|
||||||
)
|
|
||||||
|
|
||||||
BookPlaceList()
|
|
||||||
|
|
||||||
BaseButton(
|
BaseButton(
|
||||||
text = "Бронировать",
|
text = stringResource(R.string.book_back),
|
||||||
btnColor = Blue,
|
|
||||||
btnContentColor = White,
|
|
||||||
onClick = { navController.navigate(BookScreenDestination)},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(Main.ADD_BUTTON)
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 10.dp, vertical = 15.dp)
|
.testTag(Book.BACK_BUTTON),
|
||||||
.fillMaxWidth(),
|
onClick = { viewModel.onIntent(BookIntent.Back) },
|
||||||
icon = {Image(
|
btnContentColor = White,
|
||||||
painter = painterResource(R.drawable.add_icon),
|
btnColor = Blue
|
||||||
contentDescription = "plus Icon"
|
|
||||||
)}
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 = stringResource(R.string.booking_button),
|
||||||
|
btnColor = Blue,
|
||||||
|
btnContentColor = White,
|
||||||
|
onClick = { },
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(Book.BOOK_BUTTON)
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
icon = { Image(
|
||||||
|
painter = painterResource(R.drawable.add_icon),
|
||||||
|
contentDescription = stringResource(R.string.add_icon_description)
|
||||||
|
) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookPlaceList() {
|
fun BookPlaceList() {
|
||||||
BookPlaceListElement()
|
BookPlaceListElement()
|
||||||
@@ -98,32 +275,36 @@ fun BookPlaceListElement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookDateList() {
|
fun BookDateList(dates: List<String>) {
|
||||||
BookDateListElement()
|
FlowRow(
|
||||||
BookDateListElement()
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
BookDateListElement()
|
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||||
BookDateListElement()
|
modifier = Modifier.padding(vertical = 15.dp)
|
||||||
BookDateListElement()
|
) {
|
||||||
BookDateListElement()
|
dates.forEach { date ->
|
||||||
BookDateListElement()
|
BookDateListElement(date = date, onClick = {
|
||||||
|
// Обработка выбора даты
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookDateListElement() {
|
fun BookDateListElement(date: String, onClick: () -> Unit) {
|
||||||
Button(
|
Button(
|
||||||
border = BorderStroke(1.dp, Black),
|
contentPadding = PaddingValues(0.dp),
|
||||||
onClick = {},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(60.dp)
|
.testTag(Book.ITEM_DATE)
|
||||||
.padding(0.dp),
|
.padding(0.dp),
|
||||||
|
border = BorderStroke(1.dp, Black),
|
||||||
|
onClick = onClick,
|
||||||
colors = ButtonColors(
|
colors = ButtonColors(
|
||||||
contentColor = Black,
|
contentColor = Black,
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
disabledContentColor = Black,
|
disabledContentColor = Black,
|
||||||
disabledContainerColor = Color.Transparent),
|
disabledContainerColor = Color.Transparent),
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
) {
|
) {
|
||||||
BaseText16(text = "16.06")
|
val formattedDate = formatBookingDate(date)
|
||||||
|
BaseText16(text = formattedDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
object Booking: MainAction
|
||||||
|
object Auth: MainAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainIntent {
|
||||||
|
object Logout: MainIntent
|
||||||
|
object Booking: MainIntent
|
||||||
|
object LoadData: MainIntent
|
||||||
|
}
|
||||||
@@ -7,34 +7,47 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.rememberAsyncImagePainter
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
import ru.myitschool.work.core.TestIds.Main
|
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.BaseButton
|
||||||
import ru.myitschool.work.ui.BaseNoBackgroundButton
|
import ru.myitschool.work.ui.BaseNoBackgroundButton
|
||||||
import ru.myitschool.work.ui.BaseText14
|
import ru.myitschool.work.ui.BaseText14
|
||||||
|
import ru.myitschool.work.ui.BaseText16
|
||||||
import ru.myitschool.work.ui.BaseText20
|
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.Black
|
||||||
import ru.myitschool.work.ui.theme.Blue
|
import ru.myitschool.work.ui.theme.Blue
|
||||||
import ru.myitschool.work.ui.theme.LightGray
|
import ru.myitschool.work.ui.theme.LightGray
|
||||||
@@ -49,8 +62,19 @@ fun MainScreen(
|
|||||||
|
|
||||||
val state by viewModel.uiState.collectAsState()
|
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) {
|
when(state) {
|
||||||
is MainState.Loading -> {
|
is MainState.Loading -> {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -61,16 +85,59 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MainState.Error -> {
|
is MainState.Error -> {
|
||||||
|
ErrorContent(viewModel)
|
||||||
}
|
}
|
||||||
is MainState.Data -> {
|
is MainState.Data -> {
|
||||||
Content(viewModel)
|
DataContent(
|
||||||
|
viewModel,
|
||||||
|
userData = (state as MainState.Data).userData
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 (
|
Column (
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -92,27 +159,33 @@ fun Content(viewModel: MainViewModel) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
BaseNoBackgroundButton(
|
BaseNoBackgroundButton(
|
||||||
text = "Обновить",
|
text = stringResource(R.string.main_update),
|
||||||
onClick = { },
|
onClick = { viewModel.onIntent(MainIntent.LoadData) },
|
||||||
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
|
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
|
||||||
)
|
)
|
||||||
BaseNoBackgroundButton(
|
BaseNoBackgroundButton(
|
||||||
text = "Выйти",
|
text = stringResource(R.string.main_log_out),
|
||||||
onClick = { },
|
onClick = { viewModel.onIntent(MainIntent.Logout) },
|
||||||
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
|
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.avatar),
|
painter = rememberAsyncImagePainter(
|
||||||
contentDescription = "User avatar",
|
model = userData.photoUrl,
|
||||||
|
error = painterResource(R.drawable.avatar)
|
||||||
|
),
|
||||||
|
contentDescription = stringResource(R.string.main_avatar_description),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(999.dp))
|
||||||
.testTag(Main.PROFILE_IMAGE)
|
.testTag(Main.PROFILE_IMAGE)
|
||||||
|
.width(150.dp)
|
||||||
|
.height(150.dp)
|
||||||
.padding(20.dp)
|
.padding(20.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
BaseText20(
|
BaseText20(
|
||||||
text = "Артемий Артемиев Иванович",
|
text = userData.name,
|
||||||
color = White,
|
color = White,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -135,7 +208,7 @@ fun Content(viewModel: MainViewModel) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Ваши забронированные места",
|
text = stringResource(R.string.main_booking_title),
|
||||||
style = Typography.bodyMedium,
|
style = Typography.bodyMedium,
|
||||||
color = Black,
|
color = Black,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
@@ -144,20 +217,24 @@ fun Content(viewModel: MainViewModel) {
|
|||||||
vertical = 20.dp
|
vertical = 20.dp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
BookList()
|
if (userData.hasBookings()) {
|
||||||
|
SortedBookingList(userData = userData)
|
||||||
|
} else {
|
||||||
|
EmptyBookings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BaseButton(
|
BaseButton(
|
||||||
text = "Бронировать",
|
text = stringResource(R.string.booking_button),
|
||||||
btnColor = Blue,
|
btnColor = Blue,
|
||||||
btnContentColor = White,
|
btnContentColor = White,
|
||||||
onClick = {},
|
onClick = { viewModel.onIntent(MainIntent.Booking) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(Main.ADD_BUTTON)
|
.testTag(Main.ADD_BUTTON)
|
||||||
.padding(horizontal = 10.dp, vertical = 15.dp)
|
.padding(horizontal = 10.dp, vertical = 15.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
icon = {Image(
|
icon = {Image(
|
||||||
painter = painterResource(R.drawable.add_icon),
|
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
|
@Composable
|
||||||
fun BookList() {
|
fun SortedBookingList(userData: UserEntity) {
|
||||||
Column(
|
val sortedBookings = remember(userData.booking) {
|
||||||
modifier = Modifier.padding(horizontal = 20.dp)
|
userData.getSortedBookingsWithFormattedDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
) {
|
) {
|
||||||
for ((index, book) in bookListData.withIndex()) {
|
itemsIndexed(
|
||||||
BookListElement(index)
|
items = sortedBookings
|
||||||
|
) { index, (originalDate, formattedDate, bookingInfo) ->
|
||||||
|
BookingItem(
|
||||||
|
originalDate = originalDate,
|
||||||
|
formattedDate = formattedDate,
|
||||||
|
bookingInfo = bookingInfo,
|
||||||
|
index = index
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookListElement(index: Int) {
|
fun BookingItem(
|
||||||
|
originalDate: String,
|
||||||
|
formattedDate: String,
|
||||||
|
bookingInfo: BookingInfo,
|
||||||
|
index: Int
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(Main.getIdItemByPosition(index))
|
.testTag(Main.getIdItemByPosition(index))
|
||||||
@@ -192,12 +282,26 @@ fun BookListElement(index: Int) {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
BaseText14(
|
BaseText14(
|
||||||
text = "Конгресс Холл",
|
text = bookingInfo.place,
|
||||||
modifier = Modifier.testTag(Main.ITEM_PLACE)
|
modifier = Modifier.testTag(Main.ITEM_PLACE)
|
||||||
)
|
)
|
||||||
BaseText14(
|
BaseText14(
|
||||||
text = "16.02.3026",
|
text = formattedDate,
|
||||||
modifier = Modifier.testTag(Main.ITEM_DATE)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||||
|
|
||||||
sealed interface MainState {
|
sealed interface MainState {
|
||||||
object Data: MainState
|
data class Data(val userData: UserEntity): MainState
|
||||||
object Loading: MainState
|
object Loading: MainState
|
||||||
object Error: MainState
|
object Error: MainState
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,82 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
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.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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)
|
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.ui.screen.splash
|
package ru.myitschool.work.ui.screen.splash
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -28,12 +29,7 @@ class SplashViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
try {
|
try {
|
||||||
val userCode = dataStoreManager.getUserCode().first()
|
val userCode = dataStoreManager.getUserCode().first()
|
||||||
|
|
||||||
val isAuthenticated = when {
|
val isAuthenticated = if (userCode.code.isEmpty()) false else true
|
||||||
userCode == null -> false
|
|
||||||
userCode.code is String -> (userCode.code as String).isNotEmpty()
|
|
||||||
userCode.code is Int -> (userCode.code as Int) != -1
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
_splashState.value = if (isAuthenticated) {
|
_splashState.value = if (isAuthenticated) {
|
||||||
SplashState.Authenticated
|
SplashState.Authenticated
|
||||||
|
|||||||
25
app/src/main/java/ru/myitschool/work/utils.kt
Normal file
25
app/src/main/java/ru/myitschool/work/utils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,18 @@
|
|||||||
<string name="auth_title">Введите код для авторизации</string>
|
<string name="auth_title">Введите код для авторизации</string>
|
||||||
<string name="auth_label">Код</string>
|
<string name="auth_label">Код</string>
|
||||||
<string name="auth_sign_in">Войти</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user