main #6
@@ -1,7 +1,7 @@
|
|||||||
package ru.myitschool.work.core
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val HOST = "http://10.0.2.2:8080"
|
const val HOST = "http://192.168.1.39:8080"
|
||||||
const val AUTH_URL = "/auth"
|
const val AUTH_URL = "/auth"
|
||||||
const val INFO_URL = "/info"
|
const val INFO_URL = "/info"
|
||||||
const val BOOKING_URL = "/booking"
|
const val BOOKING_URL = "/booking"
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
|
||||||
|
// Не добавляйте ничего, что уже есть в Constants!
|
||||||
|
object OurConstants {
|
||||||
|
const val SHABLON = "^[a-zA-Z0-9]*\$"
|
||||||
|
const val DS_AUTH_KEY = "authkey"
|
||||||
|
}
|
||||||
11
app/src/main/java/ru/myitschool/work/core/Utils.kt
Normal file
11
app/src/main/java/ru/myitschool/work/core/Utils.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
|
import ru.myitschool.work.core.OurConstants.SHABLON
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
companion object {
|
||||||
|
fun CheckCodeInput(text : String) : Boolean{
|
||||||
|
return !text.isEmpty() && text.length == 4 && text.matches(Regex(SHABLON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/ru/myitschool/work/data/entity/Booking.kt
Normal file
11
app/src/main/java/ru/myitschool/work/data/entity/Booking.kt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.data.entity
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
|
||||||
|
data class Booking ( val id: Long,
|
||||||
|
val date: LocalDate,
|
||||||
|
val place: Place,
|
||||||
|
val employeeCode: String){
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.data.entity
|
||||||
|
|
||||||
|
data class Employee (
|
||||||
|
val name: String,
|
||||||
|
val code: String,
|
||||||
|
val photoUrl: String,
|
||||||
|
val bookingList: MutableList<Booking?>) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.myitschool.work.data.entity
|
||||||
|
|
||||||
|
data class Place(
|
||||||
|
val id: Long,
|
||||||
|
val place: String ){}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
package ru.myitschool.work.data.repo
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
object AuthRepository {
|
|
||||||
|
|
||||||
|
object AuthRepository {
|
||||||
private var codeCache: String? = null
|
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 ->
|
val result = NetworkDataSource.checkAuth(text)
|
||||||
if (success) {
|
if (result.isSuccess) {
|
||||||
codeCache = text
|
codeCache = text
|
||||||
|
createAuthCode(code = text)
|
||||||
}
|
}
|
||||||
}
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class BookingRepository {
|
||||||
|
|
||||||
|
suspend fun getAvailableBookings(): Result<Map<LocalDate, List<Place>>> {
|
||||||
|
val code = DataStoreDataSource.getAuthCode()
|
||||||
|
if (code.isEmpty() || code == "0") {
|
||||||
|
return Result.failure(Exception("Auth code not found"))
|
||||||
|
}
|
||||||
|
return NetworkDataSource.getAvailableBookings(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createBooking(date: LocalDate, placeId: Long): Result<Boolean> {
|
||||||
|
val code = DataStoreDataSource.getAuthCode()
|
||||||
|
if (code.isEmpty() || code == "0") {
|
||||||
|
return Result.failure(Exception("Auth code not found"))
|
||||||
|
}
|
||||||
|
return NetworkDataSource.createBooking(code, date, placeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Employee
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.getAuthCode
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
|
class MainRepository {
|
||||||
|
private var employee: Employee? = null
|
||||||
|
|
||||||
|
suspend fun getUserInfo(): Result<Employee> {
|
||||||
|
return try {
|
||||||
|
val code = getCode()
|
||||||
|
val result = NetworkDataSource.getUserInfo(code)
|
||||||
|
result.onSuccess { success ->
|
||||||
|
employee = success
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getCode(): String {
|
||||||
|
return getAuthCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun logOut(){
|
||||||
|
DataStoreDataSource.logOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.material3.rememberTimePickerState
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.core.OurConstants.DS_AUTH_KEY
|
||||||
|
|
||||||
|
|
||||||
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "auth")
|
||||||
|
val AUTH_KEY = stringPreferencesKey(DS_AUTH_KEY)
|
||||||
|
|
||||||
|
object DataStoreDataSource {
|
||||||
|
fun authFlow(): Flow<String> {
|
||||||
|
return App.context.dataStore.data.map { preferences ->
|
||||||
|
(preferences[AUTH_KEY] ?: 0).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createAuthCode(code: String) {
|
||||||
|
App.context.dataStore.updateData {
|
||||||
|
it.toMutablePreferences().also { preferences ->
|
||||||
|
preferences[AUTH_KEY] = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAuthCode(): String {
|
||||||
|
return App.context.dataStore.data.map { preferences ->
|
||||||
|
preferences[AUTH_KEY] ?: ""
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun logOut() {
|
||||||
|
App.context.dataStore.updateData {
|
||||||
|
it.toMutablePreferences().also { preferences ->
|
||||||
|
preferences.remove(AUTH_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
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
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
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.data.entity.Employee
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.data.entity.Booking
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
object NetworkDataSource {
|
object NetworkDataSource {
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
@@ -31,12 +45,133 @@ 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))
|
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||||
|
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> true
|
||||||
|
HttpStatusCode.Unauthorized -> error(App.context.getString(R.string.auth_wrong_code))
|
||||||
|
else -> error(App.context.getString(R.string.error_request, response.bodyAsText()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserInfo(code: String): Result<Employee> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
val json = response.bodyAsText()
|
||||||
|
if (json.isBlank()) {
|
||||||
|
error(App.context.getString(R.string.error_empty_server_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonObject = try {
|
||||||
|
Json.parseToJsonElement(json).jsonObject
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error(App.context.getString(R.string.error_parsing, e.message))
|
||||||
|
}
|
||||||
|
val name = jsonObject["name"]?.jsonPrimitive?.content
|
||||||
|
?: error(App.context.getString(R.string.error_missing_name_field))
|
||||||
|
val photoUrl = jsonObject["photoUrl"]?.jsonPrimitive?.content
|
||||||
|
?: error(App.context.getString(R.string.error_missing_photo_url_field))
|
||||||
|
|
||||||
|
val bookingJson = jsonObject["booking"]?.jsonObject
|
||||||
|
?: error(App.context.getString(R.string.error_missing_booking_field))
|
||||||
|
|
||||||
|
val employee = Employee(
|
||||||
|
name = name,
|
||||||
|
code = code,
|
||||||
|
photoUrl = photoUrl,
|
||||||
|
bookingList = mutableListOf()
|
||||||
|
)
|
||||||
|
val bookingList = mutableListOf<Booking>()
|
||||||
|
for ((dateString, bookingElement) in bookingJson) {
|
||||||
|
val date = LocalDate.parse(dateString)
|
||||||
|
val bookingObj = bookingElement.jsonObject
|
||||||
|
val bookingId = bookingObj["id"]?.jsonPrimitive?.long
|
||||||
|
?: error(App.context.getString(R.string.error_missing_id_field))
|
||||||
|
val placeString = bookingObj["place"]?.jsonPrimitive?.content
|
||||||
|
?: error(App.context.getString(R.string.error_missing_place_field, dateString))
|
||||||
|
|
||||||
|
if (placeString.isBlank()) {
|
||||||
|
error(App.context.getString(R.string.error_empty_place_field, dateString))
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeId = bookingId
|
||||||
|
val place = Place(placeId, placeString)
|
||||||
|
|
||||||
|
val booking = Booking(
|
||||||
|
id = bookingId,
|
||||||
|
date = date,
|
||||||
|
place = place,
|
||||||
|
employeeCode = employee.code
|
||||||
|
)
|
||||||
|
bookingList.add(booking)
|
||||||
|
}
|
||||||
|
/* if (bookingList.isEmpty()) {
|
||||||
|
error(App.context.getString(R.string.error_booking_list_empty))
|
||||||
|
}*/
|
||||||
|
employee.bookingList.addAll(bookingList)
|
||||||
|
employee
|
||||||
|
}
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getAvailableBookings(code: String): Result<Map<LocalDate, List<Place>>> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
val json = response.bodyAsText()
|
||||||
|
val jsonObject = Json.parseToJsonElement(json).jsonObject
|
||||||
|
val availableBookings = mutableMapOf<LocalDate, List<Place>>()
|
||||||
|
|
||||||
|
for ((dateString, placesArray) in jsonObject) {
|
||||||
|
val date = LocalDate.parse(dateString)
|
||||||
|
val places = placesArray.jsonArray.map { placeElement ->
|
||||||
|
val placeObj = placeElement.jsonObject
|
||||||
|
val id = placeObj["id"]?.jsonPrimitive?.long
|
||||||
|
?: error(App.context.getString(R.string.error_missing_id_in_place))
|
||||||
|
val placeName = placeObj["place"]?.jsonPrimitive?.content
|
||||||
|
?: error(App.context.getString(R.string.error_missing_place_in_place))
|
||||||
|
Place(id, placeName)
|
||||||
|
}
|
||||||
|
if (places.isNotEmpty()) {
|
||||||
|
availableBookings[date] = places
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availableBookings.toSortedMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> error(App.context.getString(R.string.error_request, response.bodyAsText()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class CreateBookingBody(val date: String, val placeId: Long)
|
||||||
|
|
||||||
|
suspend fun createBooking(code: String, date: LocalDate, placeId: Long): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
// Формируем тело запроса
|
||||||
|
val requestBody = CreateBookingBody(date.toString(), placeId)
|
||||||
|
|
||||||
|
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(requestBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.Created -> true
|
||||||
|
else -> {
|
||||||
|
val errorBody = response.bodyAsText()
|
||||||
|
error(if (errorBody.isNotBlank()) App.context.getString(R.string.error_booking, errorBody) else App.context.getString(R.string.error_booking_default))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,12 @@ package ru.myitschool.work.domain.auth
|
|||||||
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
class CheckAndSaveAuthCodeUseCase(
|
class CheckAndSaveAuthCodeUseCase(
|
||||||
private val repository: AuthRepository
|
private val repository: AuthRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
text: String
|
text: String
|
||||||
): Result<Unit> {
|
): Result<Boolean> {
|
||||||
return repository.checkAndSave(text).mapCatching { success ->
|
return repository.checkAndSave(text)
|
||||||
if (!success) error("Code is incorrect")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.domain.book
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.BookingRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class CreateBookingUseCase(
|
||||||
|
private val repository: BookingRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(date: LocalDate, placeId: Long): Result<Boolean> {
|
||||||
|
return repository.createBooking(date, placeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.myitschool.work.domain.book
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import ru.myitschool.work.data.repo.BookingRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class GetAvailableBookingsUseCase(
|
||||||
|
private val repository: BookingRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): Result<Map<LocalDate, List<Place>>> {
|
||||||
|
return repository.getAvailableBookings()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ru.myitschool.work.domain.main
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Employee
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.repo.MainRepository
|
||||||
|
|
||||||
|
class GetUserDataUseCase(
|
||||||
|
private val repository: MainRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): Result<Employee> {
|
||||||
|
return repository.getUserInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,15 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
|
||||||
import ru.myitschool.work.ui.screen.AppNavHost
|
import ru.myitschool.work.ui.screen.AppNavHost
|
||||||
import ru.myitschool.work.ui.theme.WorkTheme
|
import ru.myitschool.work.ui.theme.WorkTheme
|
||||||
|
|
||||||
class RootActivity : ComponentActivity() {
|
class RootActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
App.context = applicationContext
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
WorkTheme {
|
WorkTheme {
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ package ru.myitschool.work.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
|
||||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
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.screen.auth.AuthScreen
|
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
||||||
|
import ru.myitschool.work.ui.screen.auth.AuthViewModel
|
||||||
|
import ru.myitschool.work.ui.screen.book.BookScreen
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
@@ -32,18 +33,16 @@ fun AppNavHost(
|
|||||||
AuthScreen(navController = navController)
|
AuthScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable<MainScreenDestination> {
|
composable<MainScreenDestination> {
|
||||||
Box(
|
MainScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
Box(
|
BookScreen(
|
||||||
contentAlignment = Alignment.Center
|
onBack = { navController.popBackStack() },
|
||||||
) {
|
onBookSuccess = {
|
||||||
Text(text = "Hello")
|
// Возвращаемся на главный экран и обновляем его
|
||||||
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
sealed interface AuthAction {
|
||||||
|
data class ShowError(val message: String?) : AuthAction
|
||||||
|
data class LogIn(val isLogged: Boolean): AuthAction
|
||||||
|
data class AuthBtnEnabled(val enabled: Boolean) : AuthAction
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth
|
|||||||
sealed interface AuthIntent {
|
sealed interface AuthIntent {
|
||||||
data class Send(val text: String): AuthIntent
|
data class Send(val text: String): AuthIntent
|
||||||
data class TextInput(val text: String): AuthIntent
|
data class TextInput(val text: String): AuthIntent
|
||||||
|
object CheckLogIntent: AuthIntent
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -21,15 +22,18 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
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.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import io.ktor.util.collections.setValue
|
||||||
|
import ru.myitschool.work.App
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.core.OurConstants.SHABLON
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.core.Utils
|
||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -38,13 +42,11 @@ fun AuthScreen(
|
|||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.actionFlow.collect {
|
viewModel.onIntent(AuthIntent.CheckLogIntent)
|
||||||
navController.navigate(MainScreenDestination)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -58,12 +60,16 @@ fun AuthScreen(
|
|||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
is AuthState.Data -> Content(viewModel, currentState)
|
is AuthState.Data -> Content(viewModel, currentState, navController)
|
||||||
is AuthState.Loading -> {
|
is AuthState.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(64.dp)
|
modifier = Modifier.size(64.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AuthState.LoggedIn -> {
|
||||||
|
navController.navigate(MainScreenDestination)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,12 +77,36 @@ fun AuthScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
viewModel: AuthViewModel,
|
viewModel: AuthViewModel,
|
||||||
state: AuthState.Data
|
state: AuthState.Data,
|
||||||
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
var inputText by remember { mutableStateOf("") }
|
||||||
|
var errorText: String? by remember { mutableStateOf(null) }
|
||||||
|
var btnEnabled: Boolean by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val event = viewModel.actionFlow.collectAsState(initial = null)
|
||||||
|
|
||||||
|
// В UI (Composable)
|
||||||
|
val actionFlow = viewModel.actionFlow // SharedFlow<AuthAction>
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// Collect Flow<T> здесь, чтобы потреблять все события
|
||||||
|
actionFlow.collect { action ->
|
||||||
|
when (action) {
|
||||||
|
is AuthAction.ShowError -> {
|
||||||
|
errorText = action.message
|
||||||
|
}
|
||||||
|
is AuthAction.AuthBtnEnabled -> {
|
||||||
|
btnEnabled = action.enabled
|
||||||
|
} else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.CODE_INPUT)
|
||||||
|
.fillMaxWidth(),
|
||||||
value = inputText,
|
value = inputText,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
inputText = it
|
inputText = it
|
||||||
@@ -86,12 +116,20 @@ private fun Content(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||||
|
.fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
|
if (Utils.CheckCodeInput(inputText)) {
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
viewModel.onIntent(AuthIntent.Send(inputText))
|
||||||
|
} else {
|
||||||
|
errorText = App.context.getString(R.string.auth_nasty_code)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
enabled = true
|
enabled = btnEnabled
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
) { Text(stringResource(R.string.auth_sign_in)) }
|
||||||
|
if (errorText != null) {
|
||||||
|
Text(errorText.toString(), modifier = Modifier.testTag(TestIds.Auth.ERROR))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth
|
|||||||
sealed interface AuthState {
|
sealed interface AuthState {
|
||||||
object Loading: AuthState
|
object Loading: AuthState
|
||||||
object Data: AuthState
|
object Data: AuthState
|
||||||
|
object LoggedIn: AuthState
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -8,9 +10,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
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.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.core.Utils.Companion.CheckCodeInput
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel : ViewModel() {
|
||||||
@@ -18,26 +25,55 @@ class AuthViewModel : ViewModel() {
|
|||||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow(replay = 1)
|
||||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||||
|
|
||||||
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("9999").fold(
|
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
_actionFlow.emit(Unit)
|
_uiState.update { AuthState.LoggedIn }
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
error.printStackTrace()
|
error.printStackTrace()
|
||||||
_actionFlow.emit(Unit)
|
if (error.message != null) {
|
||||||
|
_actionFlow.emit(AuthAction.ShowError(error.message))
|
||||||
|
_uiState.update { AuthState.Data }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AuthIntent.TextInput -> Unit
|
|
||||||
|
is AuthIntent.TextInput -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
authFlow().collect {
|
||||||
|
if (CheckCodeInput(intent.text)) {
|
||||||
|
_actionFlow.emit(AuthAction.AuthBtnEnabled(true))
|
||||||
|
} else {
|
||||||
|
_actionFlow.emit(AuthAction.AuthBtnEnabled(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AuthIntent.CheckLogIntent -> {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { AuthState.Loading }
|
||||||
|
val authCode = authFlow().first()
|
||||||
|
if (authCode != "0") {
|
||||||
|
_actionFlow.emit(AuthAction.LogIn(true))
|
||||||
|
_uiState.update { AuthState.LoggedIn }
|
||||||
|
} else {
|
||||||
|
_uiState.update { AuthState.Data }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookAction {
|
||||||
|
data class ShowError(val message: String?) : BookAction
|
||||||
|
object BookSuccess : BookAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
sealed interface BookIntent {
|
||||||
|
object LoadData : BookIntent
|
||||||
|
object Refresh : BookIntent
|
||||||
|
object BookPlace : BookIntent
|
||||||
|
data class SelectDate(val date: LocalDate) : BookIntent
|
||||||
|
data class SelectPlace(val place: Place) : BookIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onBookSuccess: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel: BookViewModel = viewModel()
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.actionFlow) {
|
||||||
|
viewModel.actionFlow.collect { action ->
|
||||||
|
if (action is BookAction.BookSuccess) {
|
||||||
|
onBookSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.onIntent(BookIntent.LoadData)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val state = uiState) {
|
||||||
|
is BookState.Loading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BookState.Data -> {
|
||||||
|
BookContentScreen(
|
||||||
|
uiState = state,
|
||||||
|
onSelectDate = { date -> viewModel.onIntent(BookIntent.SelectDate(date)) },
|
||||||
|
onSelectPlace = { place -> viewModel.onIntent(BookIntent.SelectPlace(place)) },
|
||||||
|
onBook = { viewModel.onIntent(BookIntent.BookPlace) },
|
||||||
|
onBack = onBack,
|
||||||
|
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookContentScreen(
|
||||||
|
uiState: BookState.Data,
|
||||||
|
onSelectDate: (LocalDate) -> Unit,
|
||||||
|
onSelectPlace: (Place) -> Unit,
|
||||||
|
onBook: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onRefresh: () -> Unit
|
||||||
|
) {
|
||||||
|
val sortedDates = uiState.dates.sorted()
|
||||||
|
val availableDates = sortedDates.filter { date -> uiState.places[date]?.isNotEmpty() == true }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||||
|
if (availableDates.isNotEmpty()) {
|
||||||
|
ScrollableTabRow(
|
||||||
|
selectedTabIndex = availableDates.indexOf(uiState.selectedDate),
|
||||||
|
) {
|
||||||
|
availableDates.forEachIndexed { index, date ->
|
||||||
|
Tab(
|
||||||
|
selected = date == uiState.selectedDate,
|
||||||
|
onClick = { onSelectDate(date) },
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = date.format(DateTimeFormatter.ofPattern("dd.MM")),
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val placesForDate = uiState.selectedDate?.let { uiState.places[it] } ?: emptyList()
|
||||||
|
|
||||||
|
if (placesForDate.isNotEmpty()) {
|
||||||
|
Column {
|
||||||
|
placesForDate.forEachIndexed { index, place ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.selectable(
|
||||||
|
selected = uiState.selectedPlace == place,
|
||||||
|
onClick = { onSelectPlace(place) }
|
||||||
|
)
|
||||||
|
.testTag(TestIds.Book.getIdPlaceItemByPosition(index)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = place.place,
|
||||||
|
modifier = Modifier.weight(1f).testTag(TestIds.Book.ITEM_PLACE_TEXT)
|
||||||
|
)
|
||||||
|
RadioButton(
|
||||||
|
selected = uiState.selectedPlace == place,
|
||||||
|
onClick = { onSelectPlace(place) },
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableDates.isEmpty() && !uiState.isError) {
|
||||||
|
Text(
|
||||||
|
text = "Всё забронировано",
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.EMPTY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.isError) {
|
||||||
|
Text(
|
||||||
|
text = uiState.errorMessage ?: "Ошибка загрузки",
|
||||||
|
color = Color.Red,
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ERROR)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onRefresh,
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON)
|
||||||
|
) {
|
||||||
|
Text("Обновить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
if (!uiState.isError && placesForDate.isNotEmpty()) {
|
||||||
|
Button(
|
||||||
|
onClick = onBook,
|
||||||
|
enabled = uiState.selectedPlace != null, // активна только при выбранном месте
|
||||||
|
modifier = Modifier.fillMaxWidth().testTag(TestIds.Book.BOOK_BUTTON)
|
||||||
|
) { Text("Забронировать") }
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onBack,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag(TestIds.Book.BACK_BUTTON)
|
||||||
|
) {
|
||||||
|
Text("Назад")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
sealed interface BookState {
|
||||||
|
object Loading : BookState
|
||||||
|
data class Data(
|
||||||
|
val dates: List<LocalDate> = emptyList(),
|
||||||
|
val places: Map<LocalDate, List<Place>> = emptyMap(),
|
||||||
|
val selectedDate: LocalDate? = null,
|
||||||
|
val selectedPlace: Place? = null,
|
||||||
|
val isError: Boolean = false,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
) : BookState
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.data.entity.Place
|
||||||
|
import ru.myitschool.work.data.repo.BookingRepository
|
||||||
|
import ru.myitschool.work.domain.book.CreateBookingUseCase
|
||||||
|
import ru.myitschool.work.domain.book.GetAvailableBookingsUseCase
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class BookViewModel : ViewModel() {
|
||||||
|
private val repository by lazy { BookingRepository() }
|
||||||
|
private val getAvailableBookingsUseCase by lazy { GetAvailableBookingsUseCase(repository) }
|
||||||
|
private val createBookingUseCase by lazy { CreateBookingUseCase(repository) }
|
||||||
|
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
|
||||||
|
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actionFlow = MutableSharedFlow<BookAction>()
|
||||||
|
val actionFlow: SharedFlow<BookAction> = _actionFlow
|
||||||
|
|
||||||
|
private var selectedPlaceId: Long? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadBookData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIntent(intent: BookIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is BookIntent.LoadData -> loadBookData()
|
||||||
|
is BookIntent.Refresh -> refresh()
|
||||||
|
is BookIntent.BookPlace -> bookPlace()
|
||||||
|
is BookIntent.SelectDate -> selectDate(intent.date)
|
||||||
|
is BookIntent.SelectPlace -> selectPlace(intent.place)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadBookData() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { BookState.Loading }
|
||||||
|
|
||||||
|
getAvailableBookingsUseCase().fold(
|
||||||
|
onSuccess = { bookings ->
|
||||||
|
if (bookings.isEmpty()) {
|
||||||
|
_uiState.update {
|
||||||
|
BookState.Data(
|
||||||
|
isError = true,
|
||||||
|
errorMessage = App.context.getString(R.string.error_no_available_dates)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val dates = bookings.keys.toList()
|
||||||
|
_uiState.update {
|
||||||
|
BookState.Data(
|
||||||
|
dates = dates,
|
||||||
|
places = bookings,
|
||||||
|
selectedDate = dates.first(),
|
||||||
|
selectedPlace = null,
|
||||||
|
isError = false,
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
error.printStackTrace()
|
||||||
|
_uiState.update {
|
||||||
|
BookState.Data(
|
||||||
|
isError = true,
|
||||||
|
errorMessage = error.message ?: App.context.getString(R.string.error_loading_data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectDate(date: LocalDate) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
if (currentState is BookState.Data) {
|
||||||
|
currentState.copy(
|
||||||
|
selectedDate = date,
|
||||||
|
selectedPlace = null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedPlaceId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectPlace(place: Place) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
if (currentState is BookState.Data) {
|
||||||
|
currentState.copy(selectedPlace = place)
|
||||||
|
} else {
|
||||||
|
currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedPlaceId = place.id
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bookPlace() {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
if (currentState is BookState.Data && currentState.selectedPlace != null && currentState.selectedDate != null) {
|
||||||
|
val placeId = selectedPlaceId ?: return
|
||||||
|
val date = currentState.selectedDate
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
createBookingUseCase (date, placeId).fold(
|
||||||
|
onSuccess = {
|
||||||
|
Log.d("AnnaKonda", "method is calling")
|
||||||
|
_actionFlow.emit(BookAction.BookSuccess)
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
Log.d("AnnaKonda", "ERROR method is calling")
|
||||||
|
error.printStackTrace()
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
if (currentState is BookState.Data) {
|
||||||
|
currentState.copy(
|
||||||
|
isError = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// _actionFlow.emit(BookAction.ShowError(error.message ?: App.context.getString(R.string.error_booking_default)))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
loadBookData()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import ru.myitschool.work.ui.screen.auth.AuthAction
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
data class SetName(val name: String)
|
||||||
|
data class ShowError(val message: String?) : MainAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainIntent {
|
||||||
|
/* data class Send(val text: String): AuthIntent
|
||||||
|
data class TextInput(val text: String): AuthIntent
|
||||||
|
object CheckLogIntent: AuthIntent*/
|
||||||
|
object LoadData: MainIntent
|
||||||
|
object LogOut: MainIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.data.entity.Booking
|
||||||
|
import ru.myitschool.work.data.entity.Employee
|
||||||
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
|
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
navController: NavController,
|
||||||
|
) {
|
||||||
|
val viewModel: MainViewModel = viewModel()
|
||||||
|
// Состояния
|
||||||
|
val event = viewModel.actionFlow.collectAsState(initial = null)
|
||||||
|
// Функция загрузки данных
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.onIntent(MainIntent.LoadData)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorMessage: String? by remember { mutableStateOf("") }
|
||||||
|
LaunchedEffect(event.value) {
|
||||||
|
if (event.value is MainAction.ShowError) {
|
||||||
|
errorMessage = (event.value as MainAction.ShowError).message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если ошибка - показываем только ошибку и кнопку обновления
|
||||||
|
if (errorMessage != null) {
|
||||||
|
ErrorScreen(viewModel = viewModel, navController = navController, errorMessage)
|
||||||
|
} else {
|
||||||
|
DefaultScreen(viewModel = viewModel, navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun DefaultScreen(viewModel: MainViewModel,
|
||||||
|
navController: NavController){
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
var employee : Employee? by remember { mutableStateOf(null) }
|
||||||
|
var errorMessage by remember { mutableStateOf("") }
|
||||||
|
var bookingItems : List<Booking?>? by remember { mutableStateOf(emptyList<Booking>()) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
when (state) {
|
||||||
|
is MainState.Loading -> {
|
||||||
|
errorMessage = ""
|
||||||
|
isLoading = true
|
||||||
|
}
|
||||||
|
is MainState.Data -> {
|
||||||
|
isLoading = false
|
||||||
|
employee = (state as MainState.Data).employee
|
||||||
|
if (employee == null){
|
||||||
|
navController.navigate(AuthScreenDestination) { popUpTo(0) }
|
||||||
|
} else {
|
||||||
|
bookingItems = employee?.bookingList?.sortedBy { item ->
|
||||||
|
item?.date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
employee?.let {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Верхняя строка
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Фото пользователя (main_photo)
|
||||||
|
AsyncImage(
|
||||||
|
model = employee?.photoUrl ?: "",
|
||||||
|
contentDescription = "Фото",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.testTag(TestIds.Main.PROFILE_IMAGE),
|
||||||
|
error = painterResource(id = android.R.drawable.ic_menu_gallery)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// Имя пользователя (main_name)
|
||||||
|
Text(
|
||||||
|
text = employee!!.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
// Кнопка выхода (main_logout_button)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// Очистка данных и переход на авторизацию
|
||||||
|
viewModel.onIntent(MainIntent.LogOut)
|
||||||
|
bookingItems = emptyList()
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON)
|
||||||
|
) {
|
||||||
|
Text("Выход")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Кнопки действий
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
// Кнопка обновления (main_refresh_button)
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.LoadData) },
|
||||||
|
enabled = state !is MainState.Loading,
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
|
||||||
|
) {
|
||||||
|
if (state is MainState.Loading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text("Обновить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// кнопка бронирования
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(BookScreenDestination)
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON)
|
||||||
|
) {
|
||||||
|
Text("Перейти к бронированию")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Список бронирований
|
||||||
|
if (!bookingItems.isNullOrEmpty()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
|
||||||
|
) {
|
||||||
|
itemsIndexed(bookingItems as List<Booking?>) { index, item ->
|
||||||
|
// Элемент списка (main_book_pos_{index})
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
.testTag(TestIds.Main.getIdItemByPosition(index))
|
||||||
|
) {
|
||||||
|
// Дата бронирования (main_item_date)
|
||||||
|
Text(
|
||||||
|
text = "Дата: ${item?.date}",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Место бронирования (main_item_place)
|
||||||
|
Text(
|
||||||
|
text = "Место: ${item?.place?.place}",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Нет бронирований",
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorScreen(viewModel: MainViewModel,
|
||||||
|
navController: NavController,
|
||||||
|
errorMessage: String?){
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
// Текстовое поле с ошибкой (main_error)
|
||||||
|
|
||||||
|
if (errorMessage != null) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ERROR)
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = { viewModel.onIntent(MainIntent.LoadData) }) {
|
||||||
|
Text("Обновить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.entity.Employee
|
||||||
|
import ru.myitschool.work.ui.screen.auth.AuthState
|
||||||
|
|
||||||
|
sealed interface MainState {
|
||||||
|
object Loading: MainState
|
||||||
|
data class Data (val employee: Employee?): MainState
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.repo.MainRepository
|
||||||
|
import ru.myitschool.work.domain.main.GetUserDataUseCase
|
||||||
|
|
||||||
|
class MainViewModel : ViewModel() {
|
||||||
|
private val repository by lazy { MainRepository() }
|
||||||
|
private val getUserDataUseCase by lazy { GetUserDataUseCase(repository) }
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||||
|
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||||
|
|
||||||
|
fun onIntent(intent: MainIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is MainIntent.LoadData -> {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainIntent.LogOut -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { MainState.Data(null) }
|
||||||
|
repository.logOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadData() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { MainState.Loading }
|
||||||
|
|
||||||
|
getUserDataUseCase.invoke().fold(
|
||||||
|
onSuccess = { employee ->
|
||||||
|
_uiState.update { MainState.Data(employee) }
|
||||||
|
_actionFlow.emit(MainAction.ShowError(null))
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
error.printStackTrace()
|
||||||
|
if (error.message != null) {
|
||||||
|
_actionFlow.emit(MainAction.ShowError(error.message.toString()))
|
||||||
|
}
|
||||||
|
_uiState.update { MainState.Data(null) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,25 @@
|
|||||||
<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="auth_wrong_code">Введён неверный код</string>
|
||||||
|
<string name="auth_nasty_code">Неправильный формат кода</string>
|
||||||
|
|
||||||
|
<string name="error_request">Ошибка запроса</string>
|
||||||
|
<string name="error_empty_server_response">Пустой ответ от сервера</string>
|
||||||
|
<string name="error_parsing">Ошибка парсинга</string>
|
||||||
|
<string name="error_missing_name_field">В ответе отсутствует поле name</string>
|
||||||
|
<string name="error_missing_photo_url_field">В ответе отсутствует поле photoUrl</string>
|
||||||
|
<string name="error_missing_booking_field">В ответе отсутствует поле booking</string>
|
||||||
|
<string name="error_missing_id_field">В ответе отсутствует поле id</string>
|
||||||
|
<string name="error_missing_place_field">В ответе отсутствует поле place для даты</string>
|
||||||
|
<string name="error_empty_place_field">В ответе поле place пусто для даты</string>
|
||||||
|
<string name="error_booking_list_empty">Список бронирований пуст</string>
|
||||||
|
<string name="error_missing_id_in_place">В информации о месте отсутствует id</string>
|
||||||
|
<string name="error_missing_place_in_place">В информации о месте отсутствует place</string>
|
||||||
|
<string name="error_booking">Ошибка бронирования</string>
|
||||||
|
<string name="error_booking_default">Ошибка бронирования</string>
|
||||||
|
|
||||||
|
<string name="error_no_available_dates">Нет доступных дат для бронирования</string>
|
||||||
|
<string name="error_loading_data">Ошибка загрузки данных</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user