forked from Olympic/NTO-2025-Android-TeamTask
Compare commits
4 Commits
487d228a9b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 75ffc79666 | |||
| 4e45459af2 | |||
| 48f4f9fd6f | |||
| ca1850b97a |
@@ -36,7 +36,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
defaultComposeLibrary()
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
||||
implementation("androidx.datastore:datastore-preferences:1.2.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||
val coil = "3.3.0"
|
||||
|
||||
@@ -2,11 +2,21 @@ package ru.myitschool.work
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import ru.myitschool.work.data.datastore.DataStoreManager
|
||||
|
||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "datastore")
|
||||
|
||||
class App: Application() {
|
||||
|
||||
lateinit var dataStoreManager: DataStoreManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
dataStoreManager = DataStoreManager(dataStore)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package ru.myitschool.work.data.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class DataStoreManager(
|
||||
private val dataStore: DataStore<Preferences>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserCode(): Flow<UserCode> = dataStore.data.map { preferences ->
|
||||
UserCode(
|
||||
code = preferences[USER_CODE_KEY] ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.myitschool.work.data.datastore
|
||||
|
||||
data class UserCode(
|
||||
val code: String
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
|
||||
suspend fun bookPlace(
|
||||
userCode: String,
|
||||
date: String,
|
||||
placeId: Int,
|
||||
placeName: String
|
||||
): Result<Unit> {
|
||||
return NetworkDataSource.bookPlace(userCode, date, placeId, placeName)
|
||||
}
|
||||
}
|
||||
@@ -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,9 +1,13 @@
|
||||
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
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
@@ -11,6 +15,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 {
|
||||
@@ -28,9 +56,38 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bookPlace(
|
||||
userCode: String,
|
||||
date: String,
|
||||
placeId: Int,
|
||||
placeName: String
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
// Log.i("aaa", "Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName")
|
||||
// println("Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName")
|
||||
|
||||
val response = client.post(getUrl(userCode, Constants.BOOK_URL)) {
|
||||
setBody(mapOf(
|
||||
"date" to date,
|
||||
"placeId" to placeId,
|
||||
"placeName" to placeName
|
||||
))
|
||||
}
|
||||
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> Unit
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
|
||||
// true // удалить при проверке
|
||||
|
||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||
response.status
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> true
|
||||
else -> error(response.bodyAsText())
|
||||
@@ -38,5 +95,35 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.myitschool.work.domain.book
|
||||
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
|
||||
class BookingUseCase(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
userCode: String,
|
||||
date: String,
|
||||
placeId: Int,
|
||||
placeName: String
|
||||
): Result<Unit> {
|
||||
return repository.bookPlace(userCode, date, placeId, placeName)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -2,14 +2,9 @@ package ru.myitschool.work.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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
|
||||
@@ -17,19 +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.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
|
||||
@@ -131,13 +122,13 @@ fun BaseText14(
|
||||
fun BaseInputText(
|
||||
placeholder: String= "",
|
||||
modifier: Modifier = Modifier,
|
||||
valueChange: (String) -> Unit,
|
||||
onValueChange: (String) -> Unit,
|
||||
value: String
|
||||
) {
|
||||
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = valueChange,
|
||||
onValueChange = onValueChange,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
placeholder = {
|
||||
BaseText16(
|
||||
@@ -179,6 +170,7 @@ fun BaseText20(
|
||||
|
||||
@Composable
|
||||
fun BaseButton(
|
||||
border: BorderStroke? = null,
|
||||
enable: Boolean = true,
|
||||
text: String,
|
||||
btnColor: Color,
|
||||
@@ -188,6 +180,7 @@ fun BaseButton(
|
||||
modifier: Modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Button(
|
||||
border = border,
|
||||
enabled = enable,
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -202,33 +195,4 @@ fun BaseButton(
|
||||
icon()
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
|
||||
@Serializable
|
||||
data object MainScreenDestination: AppDestination {
|
||||
val userData = ""
|
||||
}
|
||||
data object MainScreenDestination: AppDestination
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object SplashScreenDestination: AppDestination
|
||||
@@ -14,9 +14,11 @@ import androidx.navigation.compose.rememberNavController
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import ru.myitschool.work.ui.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
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
@@ -28,21 +30,19 @@ fun AppNavHost(
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
navController = navController,
|
||||
startDestination = AuthScreenDestination,
|
||||
startDestination = SplashScreenDestination,
|
||||
) {
|
||||
composable<SplashScreenDestination> {
|
||||
SplashScreen(navController = navController)
|
||||
}
|
||||
composable<AuthScreenDestination> {
|
||||
AuthScreen(
|
||||
navController = navController)
|
||||
AuthScreen(navController = navController)
|
||||
}
|
||||
composable<MainScreenDestination> {
|
||||
MainScreen(
|
||||
navController = navController
|
||||
)
|
||||
MainScreen(navController = navController)
|
||||
}
|
||||
composable<BookScreenDestination> {
|
||||
BookScreen(
|
||||
navController = navController
|
||||
)
|
||||
BookScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,24 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.R
|
||||
@@ -46,130 +36,95 @@ import ru.myitschool.work.ui.theme.White
|
||||
@Composable
|
||||
fun AuthScreen(
|
||||
viewModel: AuthViewModel = viewModel(),
|
||||
navController: NavController
|
||||
navController: NavController,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(200.dp)
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect {
|
||||
navController.navigate(MainScreenDestination)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Logo()
|
||||
|
||||
BaseText24(
|
||||
text = stringResource(R.string.auth_title),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 20.dp)
|
||||
modifier = Modifier
|
||||
.width(400.dp)
|
||||
.fillMaxHeight()
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
val textState by viewModel.textState.collectAsStateWithLifecycle()
|
||||
val errorState by viewModel.errorState.collectAsStateWithLifecycle()
|
||||
Logo()
|
||||
|
||||
BaseInputText(
|
||||
value = textState,
|
||||
placeholder = stringResource(R.string.auth_label),
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.CODE_INPUT)
|
||||
.fillMaxWidth(),
|
||||
valueChange = {viewModel.updateText(it)}
|
||||
BaseText24(
|
||||
text = stringResource(R.string.auth_title),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (errorState) {
|
||||
BaseText12(
|
||||
text = "Недействительный код сотрудника",
|
||||
color = Red,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.ERROR)
|
||||
.padding(
|
||||
start = 10.dp,
|
||||
top = 5.dp,
|
||||
bottom = 0.dp
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
when (state) {
|
||||
is AuthState.Data -> Content(viewModel)
|
||||
is AuthState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(top = 40.dp)
|
||||
.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isButtonEnabled by viewModel.isButtonEnabled.collectAsStateWithLifecycle()
|
||||
|
||||
BaseButton(
|
||||
text = stringResource(R.string.auth_sign_in),
|
||||
onClick = { navController.navigate(MainScreenDestination)},
|
||||
btnColor = Blue,
|
||||
enable = isButtonEnabled,
|
||||
btnContentColor = White,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewModel: AuthViewModel
|
||||
) {
|
||||
|
||||
//@Composable
|
||||
//fun AuthScreen(
|
||||
// viewModel: AuthViewModel = viewModel(),
|
||||
// navController: NavController
|
||||
//) {
|
||||
// val state by viewModel.uiState.collectAsState()
|
||||
//
|
||||
// LaunchedEffect(Unit) {
|
||||
// viewModel.actionFlow.collect {
|
||||
// navController.navigate(MainScreenDestination)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .padding(all = 24.dp),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally,
|
||||
// verticalArrangement = Arrangement.Center
|
||||
// ) {
|
||||
// Text(
|
||||
// text = stringResource(R.string.auth_title),
|
||||
// style = MaterialTheme.typography.headlineSmall,
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// when (val currentState = state) {
|
||||
// is AuthState.Data -> Content(viewModel, currentState)
|
||||
// is AuthState.Loading -> {
|
||||
// CircularProgressIndicator(
|
||||
// modifier = Modifier.size(64.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@Composable
|
||||
//private fun Content(
|
||||
// viewModel: AuthViewModel,
|
||||
// state: AuthState.Data
|
||||
//) {
|
||||
// var inputText by remember { mutableStateOf("") }
|
||||
// Spacer(modifier = Modifier.size(16.dp))
|
||||
// TextField(
|
||||
// modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
// value = inputText,
|
||||
// onValueChange = {
|
||||
// inputText = it
|
||||
// viewModel.onIntent(AuthIntent.TextInput(it))
|
||||
// },
|
||||
// label = { Text(stringResource(R.string.auth_label)) }
|
||||
// )
|
||||
// Spacer(modifier = Modifier.size(16.dp))
|
||||
// Button(
|
||||
// modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||
// onClick = {
|
||||
// viewModel.onIntent(AuthIntent.Send(inputText))
|
||||
// },
|
||||
// enabled = true
|
||||
// ) {
|
||||
// Text(stringResource(R.string.auth_sign_in))
|
||||
// }
|
||||
//}
|
||||
val isButtonEnabled by viewModel.isButtonEnabled.collectAsState()
|
||||
val errorStateValue by viewModel.errorStateValue.collectAsState()
|
||||
val textState by viewModel.textState.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 20.dp)
|
||||
) {
|
||||
|
||||
BaseInputText(
|
||||
value = textState,
|
||||
placeholder = stringResource(R.string.auth_label),
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.CODE_INPUT)
|
||||
.fillMaxWidth(),
|
||||
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) }
|
||||
)
|
||||
|
||||
if (errorStateValue != "") {
|
||||
BaseText12(
|
||||
text = errorStateValue,
|
||||
color = Red,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.ERROR)
|
||||
.padding(
|
||||
start = 10.dp,
|
||||
top = 5.dp,
|
||||
bottom = 0.dp
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BaseButton(
|
||||
text = stringResource(R.string.auth_sign_in),
|
||||
onClick = { viewModel.onIntent(AuthIntent.Send(textState)) },
|
||||
btnColor = Blue,
|
||||
enable = isButtonEnabled,
|
||||
btnContentColor = White,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -1,66 +1,62 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.App
|
||||
import ru.myitschool.work.data.datastore.UserCode
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
// private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
// private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||
// val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
//
|
||||
// private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
// val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
//
|
||||
// fun onIntent(intent: AuthIntent) {
|
||||
// when (intent) {
|
||||
// is AuthIntent.Send -> {
|
||||
// viewModelScope.launch(Dispatchers.Default) {
|
||||
// _uiState.update { AuthState.Loading }
|
||||
// checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
||||
// onSuccess = {
|
||||
// _actionFlow.emit(Unit)
|
||||
// },
|
||||
// onFailure = { error ->
|
||||
// error.printStackTrace()
|
||||
// _actionFlow.emit(Unit)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// is AuthIntent.TextInput -> Unit
|
||||
// }
|
||||
// }
|
||||
class AuthViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val dataStoreManager by lazy {
|
||||
(getApplication() as App).dataStoreManager
|
||||
}
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
private val _errorStateValue = MutableStateFlow("")
|
||||
val errorStateValue: StateFlow<String> = _errorStateValue.asStateFlow()
|
||||
private val _isButtonEnabled = MutableStateFlow(false)
|
||||
val isButtonEnabled: StateFlow<Boolean> = _isButtonEnabled.asStateFlow()
|
||||
private val _textState = MutableStateFlow("")
|
||||
val textState: StateFlow<String> = _textState.asStateFlow()
|
||||
|
||||
private val _errorState = MutableStateFlow(false)
|
||||
val errorState: StateFlow<Boolean> = _errorState.asStateFlow()
|
||||
|
||||
fun updateText(newText: String) {
|
||||
_textState.value = newText
|
||||
_errorState.value = false
|
||||
fun onIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
is AuthIntent.Send -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_uiState.update { AuthState.Loading }
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||
onSuccess = {
|
||||
dataStoreManager.saveUserCode(UserCode(code = intent.text))
|
||||
_actionFlow.emit(Unit)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
_uiState.update { AuthState.Data }
|
||||
_errorStateValue.value = error.message.toString() ?: "Неизвестная ошибка"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AuthIntent.TextInput -> {
|
||||
_textState.value = intent.text
|
||||
_errorStateValue.value = ""
|
||||
_isButtonEnabled.value = if (intent.text.length == 4 && intent.text.matches(Regex("^[a-zA-Z0-9]*\$")))
|
||||
true else false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isButtonEnabled: StateFlow<Boolean> =
|
||||
_textState.map { it.length == 4 && it.matches(Regex("^[a-zA-Z0-9]*\$"))}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookAction {
|
||||
object Auth: BookAction
|
||||
object Main: BookAction
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookIntent {
|
||||
object Back: BookIntent
|
||||
object LoadBooking: BookIntent
|
||||
object Book : BookIntent
|
||||
data class SelectDate(val date: String) : BookIntent
|
||||
data class SelectPlace(
|
||||
val placeId: Int,
|
||||
val placeName: String
|
||||
) : BookIntent
|
||||
}
|
||||
@@ -1,129 +1,424 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds.Main
|
||||
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.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.White
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
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.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.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonColors
|
||||
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.domain.book.entities.PlaceInfo
|
||||
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.BaseText24
|
||||
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.Typography
|
||||
import ru.myitschool.work.ui.theme.White
|
||||
|
||||
@Composable
|
||||
fun BookScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
Column(
|
||||
@Composable
|
||||
fun BookScreen(
|
||||
navController: NavController,
|
||||
viewModel: BookViewModel = viewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
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()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BookState.Data -> {
|
||||
val dataState = state as BookState.Data
|
||||
DataContent(
|
||||
viewModel = viewModel,
|
||||
bookingData = dataState.userBooking,
|
||||
selectedDate = dataState.selectedDate,
|
||||
selectedPlaceId = dataState.selectedPlaceId
|
||||
)
|
||||
}
|
||||
is BookState.Error -> ErrorContent(viewModel)
|
||||
is BookState.Empty -> EmptyContent(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyContent(
|
||||
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_all_booked),
|
||||
modifier = Modifier.testTag(Book.EMPTY),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
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,
|
||||
selectedDate: String,
|
||||
selectedPlaceId: Int
|
||||
) {
|
||||
|
||||
val availableDates = bookingData.bookings
|
||||
.filter { it.value.isNotEmpty() }
|
||||
.keys
|
||||
.sorted()
|
||||
val placesForSelectedDate = bookingData.bookings[selectedDate] ?: emptyList()
|
||||
|
||||
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(
|
||||
dates = availableDates,
|
||||
selectedDate = selectedDate,
|
||||
onDateSelected = { date ->
|
||||
viewModel.onIntent(BookIntent.SelectDate(date))
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.book_choose_place),
|
||||
style = Typography.bodyMedium,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
|
||||
BookPlaceList(
|
||||
places = placesForSelectedDate,
|
||||
selectedPlaceId = selectedPlaceId,
|
||||
onPlaceSelected = { placeId, placeName ->
|
||||
viewModel.onIntent(BookIntent.SelectPlace(placeId, placeName))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BaseButton(
|
||||
enable = selectedPlaceId != -1,
|
||||
text = stringResource(R.string.booking_button),
|
||||
btnColor = Blue,
|
||||
btnContentColor = White,
|
||||
onClick = { viewModel.onIntent(BookIntent.Book) },
|
||||
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
|
||||
fun BookPlaceList(
|
||||
places: List<PlaceInfo>,
|
||||
selectedPlaceId: Int,
|
||||
onPlaceSelected: (Int, String) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (places.isEmpty()) {
|
||||
Text(
|
||||
text = "Нет доступных мест для выбранной даты",
|
||||
color = Color.Gray,
|
||||
style = Typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
} else {
|
||||
places.forEachIndexed { index, placeInfo ->
|
||||
BookPlaceListElement(
|
||||
placeInfo = placeInfo,
|
||||
isSelected = placeInfo.id == selectedPlaceId,
|
||||
onPlaceSelected = { onPlaceSelected(placeInfo.id, placeInfo.place) },
|
||||
index = index
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookPlaceListElement(
|
||||
placeInfo: PlaceInfo,
|
||||
isSelected: Boolean,
|
||||
onPlaceSelected: () -> Unit,
|
||||
index: Int
|
||||
) {
|
||||
Row(
|
||||
|
||||
) {
|
||||
BaseText24(
|
||||
text = "Новая встреча"
|
||||
)
|
||||
BaseNoBackgroundButton(
|
||||
text = "Назад",
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
Column(
|
||||
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = onPlaceSelected
|
||||
)
|
||||
.testTag(Book.getIdPlaceItemByPosition(index))
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
BaseText16(
|
||||
text = "Доступные даты"
|
||||
text = placeInfo.place,
|
||||
modifier = Modifier.testTag(Book.ITEM_PLACE_TEXT)
|
||||
)
|
||||
|
||||
BookDateList()
|
||||
|
||||
BaseText16(
|
||||
text = "Выберите место встречи"
|
||||
)
|
||||
|
||||
BookPlaceList()
|
||||
|
||||
BaseButton(
|
||||
text = "Бронировать",
|
||||
btnColor = Blue,
|
||||
btnContentColor = White,
|
||||
onClick = { navController.navigate(BookScreenDestination)},
|
||||
Box(
|
||||
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"
|
||||
)}
|
||||
|
||||
)
|
||||
.size(24.dp)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = if (isSelected) Blue else Color.Gray,
|
||||
shape = CircleShape
|
||||
)
|
||||
.background(
|
||||
color = if (isSelected) Blue else Color.Transparent,
|
||||
shape = CircleShape
|
||||
)
|
||||
.testTag(Book.ITEM_PLACE_SELECTOR)
|
||||
) {
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(Color.White, CircleShape)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookPlaceList() {
|
||||
BookPlaceListElement()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookPlaceListElement() {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookDateList() {
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
BookDateListElement()
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookDateListElement() {
|
||||
Button(
|
||||
border = BorderStroke(1.dp, Black),
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.width(60.dp)
|
||||
.padding(0.dp),
|
||||
colors = ButtonColors(
|
||||
contentColor = Black,
|
||||
containerColor = Color.Transparent,
|
||||
disabledContentColor = Black,
|
||||
disabledContainerColor = Color.Transparent),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
@Composable
|
||||
fun BookDateList(
|
||||
dates: List<String>,
|
||||
selectedDate: String,
|
||||
onDateSelected: (String) -> Unit
|
||||
) {
|
||||
BaseText16(text = "16.06")
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
modifier = Modifier.padding(vertical = 15.dp)
|
||||
) {
|
||||
dates.forEachIndexed { index, date ->
|
||||
BookDateListElement(
|
||||
date = date,
|
||||
isSelected = date == selectedDate,
|
||||
onClick = { onDateSelected(date) },
|
||||
index = index
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BookDateListElement(
|
||||
date: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
index: Int
|
||||
) {
|
||||
Button(
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier
|
||||
.testTag(Book.getIdDateItemByPosition(index))
|
||||
.padding(0.dp),
|
||||
border = BorderStroke(1.dp, if (isSelected) Blue else Black,),
|
||||
onClick = onClick,
|
||||
colors = ButtonColors(
|
||||
contentColor = if (isSelected) White else Black,
|
||||
containerColor = if (isSelected) Blue else Color.Transparent,
|
||||
disabledContentColor = Black,
|
||||
disabledContainerColor = Color.Transparent),
|
||||
) {
|
||||
val formattedDate = date.formatBookingDate()
|
||||
BaseText16(
|
||||
text = formattedDate,
|
||||
modifier = Modifier.testTag(Book.ITEM_DATE),
|
||||
color = if (isSelected) White else Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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,
|
||||
val selectedDate: String = "",
|
||||
val selectedPlaceId: Int = -1,
|
||||
val selectedPlaceName: String = ""
|
||||
): BookState
|
||||
object Error: BookState
|
||||
object Empty: BookState
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
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.data.repo.MainRepository
|
||||
import ru.myitschool.work.domain.book.BookingUseCase
|
||||
import ru.myitschool.work.domain.book.LoadBookingUseCase
|
||||
import ru.myitschool.work.domain.main.LoadDataUseCase
|
||||
import ru.myitschool.work.ui.screen.main.MainAction
|
||||
import ru.myitschool.work.ui.screen.main.MainIntent
|
||||
import ru.myitschool.work.ui.screen.main.MainState
|
||||
import kotlin.text.isEmpty
|
||||
|
||||
class BookViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) }
|
||||
|
||||
private val bookingUseCase by lazy { BookingUseCase (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 bookSelectedPlace() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val userCode = dataStoreManager.getUserCode().first()
|
||||
val currentState = _uiState.value
|
||||
|
||||
if (currentState is BookState.Data && currentState.selectedPlaceId != -1) {
|
||||
bookingUseCase.invoke(
|
||||
userCode = userCode.code,
|
||||
date = currentState.selectedDate,
|
||||
placeId = currentState.selectedPlaceId,
|
||||
placeName = currentState.selectedPlaceName
|
||||
).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(BookAction.Main)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
_uiState.update { BookState.Error }
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
error.printStackTrace()
|
||||
_uiState.update { BookState.Error }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
val availableDates = data.bookings
|
||||
.filter { it.value.isNotEmpty() }
|
||||
.keys
|
||||
.sorted()
|
||||
|
||||
if (availableDates.isEmpty()) {
|
||||
_uiState.update { BookState.Empty }
|
||||
} else {
|
||||
val selectedDate = availableDates.first()
|
||||
val placesForSelectedDate = data.bookings[selectedDate] ?: emptyList()
|
||||
val selectedPlaceId = placesForSelectedDate.firstOrNull()?.id ?: -1
|
||||
val selectedPlaceName = placesForSelectedDate.firstOrNull()?.place ?: ""
|
||||
|
||||
_uiState.update {
|
||||
BookState.Data(
|
||||
userBooking = data,
|
||||
selectedDate = selectedDate,
|
||||
selectedPlaceId = selectedPlaceId,
|
||||
selectedPlaceName = selectedPlaceName
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
is BookIntent.Book -> bookSelectedPlace()
|
||||
|
||||
is BookIntent.SelectDate -> {
|
||||
val currentState = _uiState.value
|
||||
if (currentState is BookState.Data) {
|
||||
val placesForDate =
|
||||
currentState.userBooking.bookings[intent.date] ?: emptyList()
|
||||
val newSelectedPlaceId = placesForDate.firstOrNull()?.id ?: -1
|
||||
val newSelectedPlaceName = placesForDate.firstOrNull()?.place ?: ""
|
||||
|
||||
_uiState.update {
|
||||
currentState.copy(
|
||||
selectedDate = intent.date,
|
||||
selectedPlaceId = newSelectedPlaceId,
|
||||
selectedPlaceName = newSelectedPlaceName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookIntent.SelectPlace -> {
|
||||
val currentState = _uiState.value
|
||||
if (currentState is BookState.Data) {
|
||||
_uiState.update {
|
||||
currentState.copy(
|
||||
selectedPlaceId = intent.placeId,
|
||||
selectedPlaceName = intent.placeName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,32 +7,46 @@ 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.ErrorScreen
|
||||
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
|
||||
@@ -42,7 +56,87 @@ import ru.myitschool.work.ui.theme.White
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
navController: NavController
|
||||
navController: NavController,
|
||||
viewModel: MainViewModel = viewModel()
|
||||
) {
|
||||
|
||||
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()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
is MainState.Error -> {
|
||||
ErrorContent(viewModel)
|
||||
}
|
||||
is MainState.Data -> {
|
||||
DataContent(
|
||||
viewModel,
|
||||
userData = (state as MainState.Data).userData
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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,
|
||||
@@ -52,121 +146,134 @@ fun MainScreen(
|
||||
.width(400.dp)
|
||||
|
||||
) {
|
||||
|
||||
val dateError = false
|
||||
|
||||
if(!dateError) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
|
||||
.background(Blue)
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
|
||||
.background(Blue)
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
|
||||
BaseNoBackgroundButton(
|
||||
text = "Обновить",
|
||||
onClick = { },
|
||||
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
|
||||
)
|
||||
|
||||
BaseNoBackgroundButton(
|
||||
text = "Выйти",
|
||||
onClick = { },
|
||||
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
|
||||
)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.avatar),
|
||||
contentDescription = "User avatar",
|
||||
modifier = Modifier
|
||||
.testTag(Main.PROFILE_IMAGE)
|
||||
.padding(20.dp)
|
||||
BaseNoBackgroundButton(
|
||||
text = stringResource(R.string.main_update),
|
||||
onClick = { viewModel.onIntent(MainIntent.LoadData) },
|
||||
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
|
||||
)
|
||||
|
||||
BaseText20(
|
||||
text = "Артемий Артемиев Иванович",
|
||||
color = White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.testTag(Main.PROFILE_NAME)
|
||||
.width(250.dp),
|
||||
style = Typography.bodyLarge
|
||||
BaseNoBackgroundButton(
|
||||
text = stringResource(R.string.main_log_out),
|
||||
onClick = { viewModel.onIntent(MainIntent.Logout) },
|
||||
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = userData.photoUrl,
|
||||
error = painterResource(R.drawable.avatar)
|
||||
),
|
||||
contentDescription = stringResource(R.string.main_avatar_description),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.testTag(Main.PROFILE_IMAGE)
|
||||
.width(150.dp)
|
||||
.height(150.dp)
|
||||
.padding(20.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(White)
|
||||
)
|
||||
|
||||
BaseText20(
|
||||
text = userData.name,
|
||||
color = White,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.testTag(Main.PROFILE_NAME)
|
||||
.width(250.dp),
|
||||
style = Typography.bodyLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(20.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Ваши забронированные места",
|
||||
style = Typography.bodyMedium,
|
||||
color = Black,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 10.dp,
|
||||
vertical = 20.dp
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.main_booking_title),
|
||||
style = Typography.bodyMedium,
|
||||
color = Black,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 10.dp,
|
||||
vertical = 20.dp
|
||||
)
|
||||
BookList()
|
||||
}
|
||||
BaseButton(
|
||||
text = "Бронировать",
|
||||
btnColor = Blue,
|
||||
btnContentColor = White,
|
||||
onClick = { navController.navigate(BookScreenDestination)},
|
||||
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"
|
||||
)}
|
||||
|
||||
)
|
||||
if (userData.hasBookings()) {
|
||||
SortedBookingList(userData = userData)
|
||||
} else {
|
||||
EmptyBookings()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ErrorScreen()
|
||||
BaseButton(
|
||||
text = stringResource(R.string.booking_button),
|
||||
btnColor = Blue,
|
||||
btnContentColor = White,
|
||||
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 = stringResource(R.string.add_icon_description)
|
||||
)}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -175,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||
|
||||
sealed interface MainState {
|
||||
data class Data(val userData: UserEntity): MainState
|
||||
object Loading: MainState
|
||||
object Error: MainState
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
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(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ru.myitschool.work.ui.screen.splash
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(
|
||||
navController: NavController,
|
||||
viewModel: SplashViewModel = viewModel()
|
||||
) {
|
||||
|
||||
val splashState by viewModel.splashState.collectAsState()
|
||||
|
||||
LaunchedEffect(splashState) {
|
||||
when (splashState) {
|
||||
is SplashState.Authenticated -> {
|
||||
navController.navigate(MainScreenDestination)
|
||||
}
|
||||
is SplashState.UnAuthenticated -> {
|
||||
navController.navigate(AuthScreenDestination)
|
||||
}
|
||||
is SplashState.Error -> {
|
||||
navController.navigate(AuthScreenDestination)
|
||||
}
|
||||
SplashState.Loading -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(64.dp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.myitschool.work.ui.screen.splash
|
||||
|
||||
import android.os.Message
|
||||
|
||||
sealed interface SplashState {
|
||||
object Loading: SplashState
|
||||
object Authenticated: SplashState
|
||||
object UnAuthenticated: SplashState
|
||||
class Error(message: String): SplashState
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.App
|
||||
|
||||
class SplashViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val dataStoreManager by lazy {
|
||||
(getApplication() as App).dataStoreManager
|
||||
}
|
||||
|
||||
private val _splashState = MutableStateFlow<SplashState>(SplashState.Loading)
|
||||
val splashState: StateFlow<SplashState> = _splashState.asStateFlow()
|
||||
|
||||
init {
|
||||
checkAuthStatus()
|
||||
}
|
||||
|
||||
private fun checkAuthStatus() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val userCode = dataStoreManager.getUserCode().first()
|
||||
|
||||
val isAuthenticated = if (userCode.code.isEmpty()) false else true
|
||||
|
||||
_splashState.value = if (isAuthenticated) {
|
||||
SplashState.Authenticated
|
||||
} else {
|
||||
SplashState.UnAuthenticated
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_splashState.value = SplashState.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
app/src/main/java/ru/myitschool/work/utils.kt
Normal file
26
app/src/main/java/ru/myitschool/work/utils.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package ru.myitschool.work
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
fun String.formatDate(): String {
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
|
||||
val date = inputFormat.parse(this)
|
||||
outputFormat.format(date)
|
||||
} catch (e: Exception) {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun String.formatBookingDate(): String {
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault())
|
||||
val date = inputFormat.parse(this)
|
||||
outputFormat.format(date)
|
||||
} catch (e: Exception) {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user