forked from Olympic/NTO-2025-Android-TeamTask
Before test crutches
This commit is contained in:
36
app/src/main/java/ru/myitschool/work/core/DateUtils.kt
Normal file
36
app/src/main/java/ru/myitschool/work/core/DateUtils.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package ru.myitschool.work.core
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object DateUtils {
|
||||
|
||||
private val mainFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||
private val bookFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("dd.MM")
|
||||
|
||||
/** Пытаемся распарсить дату в нескольких форматах */
|
||||
fun parseDate(raw: String): LocalDate? {
|
||||
return runCatching { LocalDate.parse(raw) }.getOrElse {
|
||||
runCatching { OffsetDateTime.parse(raw).toLocalDate() }.getOrElse {
|
||||
runCatching {
|
||||
LocalDate.parse(raw, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Формат для главного экрана: dd.MM.yyyy */
|
||||
fun formatForMain(raw: String): String {
|
||||
val date = parseDate(raw) ?: return raw
|
||||
return date.format(mainFormatter)
|
||||
}
|
||||
|
||||
/** Формат для экрана бронирования: dd.MM */
|
||||
fun formatForBook(raw: String): String {
|
||||
val date = parseDate(raw) ?: return raw
|
||||
return date.format(bookFormatter)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,52 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import ru.myitschool.work.App
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object AuthRepository {
|
||||
|
||||
private val dataStore get() = App.context.authDataStore
|
||||
private val KEY_CODE = stringPreferencesKey("auth_code")
|
||||
private var codeCache: String? = null
|
||||
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
// suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
// return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
// if (success) {
|
||||
// codeCache = text
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
dataStore.edit { prefs ->
|
||||
prefs[KEY_CODE] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Сохранённый код (из кэша или DataStore) */
|
||||
suspend fun getSavedCode(): String? {
|
||||
codeCache?.let { return it }
|
||||
|
||||
val code = dataStore.data
|
||||
.map { prefs -> prefs[KEY_CODE] }
|
||||
.first()
|
||||
|
||||
codeCache = code
|
||||
return code
|
||||
}
|
||||
|
||||
/** Полная очистка данных авторизации (для logout) */
|
||||
suspend fun clear() {
|
||||
codeCache = null
|
||||
dataStore.edit { prefs ->
|
||||
prefs.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import java.time.LocalDate
|
||||
import ru.myitschool.work.core.DateUtils
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
import ru.myitschool.work.data.source.dto.UserDto
|
||||
import ru.myitschool.work.domain.entities.BookingEntity
|
||||
import ru.myitschool.work.domain.entities.UserEntity
|
||||
|
||||
|
||||
object UserRepository {
|
||||
|
||||
suspend fun getUserInfo(): Result<UserEntity> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
|
||||
return NetworkDataSource.getUserInfo(code).map { dto ->
|
||||
val bookings = dto.booking
|
||||
.map { it.toDomain() }
|
||||
.sortedWith(
|
||||
compareBy<BookingEntity> { booking ->
|
||||
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||
}.thenBy { it.time }
|
||||
)
|
||||
|
||||
UserEntity(
|
||||
name = dto.name,
|
||||
bookings = bookings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
|
||||
return NetworkDataSource.getAvailableBookings(code).map { list ->
|
||||
list.map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun book(room: String, time: String): Result<Unit> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
|
||||
return NetworkDataSource.book(code, room, time)
|
||||
}
|
||||
|
||||
private fun UserDto.BookingDto.toDomain(): BookingEntity =
|
||||
BookingEntity(
|
||||
roomName = room,
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package ru.myitschool.work.data.source
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
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 +16,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.myitschool.work.core.Constants
|
||||
import ru.myitschool.work.data.source.dto.UserDto
|
||||
|
||||
object NetworkDataSource {
|
||||
private val client by lazy {
|
||||
@@ -38,5 +44,52 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun getUserInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> response.body<UserDto>()
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAvailableBookings(
|
||||
code: String
|
||||
): Result<List<UserDto.BookingDto>> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun book(
|
||||
code: String,
|
||||
room: String,
|
||||
time: String
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||
setBody(
|
||||
MultiPartFormDataContent(
|
||||
formData {
|
||||
append("room", room)
|
||||
append("time", time)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> Unit
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.myitschool.work.data.source.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
@SerialName("name")
|
||||
val name: String = "Administrator",
|
||||
@SerialName("booking")
|
||||
val booking: List<BookingDto>
|
||||
) {
|
||||
@Serializable
|
||||
data class BookingDto(
|
||||
@SerialName("room")
|
||||
val room: String,
|
||||
@SerialName("time")
|
||||
val time: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class ClearAuthDataUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class GetSavedAuthCodeUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(): String? {
|
||||
return repository.getSavedCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.booking
|
||||
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
|
||||
class BookPlaceUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
suspend operator fun invoke(room: String, time: String): Result<Unit> {
|
||||
return repository.book(room, time)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.booking
|
||||
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
import ru.myitschool.work.domain.entities.BookingEntity
|
||||
|
||||
class GetAvailableBookingsUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<List<BookingEntity>> {
|
||||
return repository.getAvailableBookings()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.domain.entities
|
||||
|
||||
data class BookingEntity(
|
||||
val roomName: String,
|
||||
val time: String,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.domain.entities
|
||||
|
||||
data class UserEntity(
|
||||
val name: String,
|
||||
val bookings: List<BookingEntity>,
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
package ru.myitschool.work.domain.user
|
||||
|
||||
class GetAvailableBookingsUseCase {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.user
|
||||
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
import ru.myitschool.work.domain.entities.UserEntity
|
||||
|
||||
class GetUserInfoUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<UserEntity> {
|
||||
return repository.getUserInfo()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
sealed interface AuthState {
|
||||
object Loading: AuthState
|
||||
object Data: AuthState
|
||||
object Loading : AuthState
|
||||
|
||||
data class Data(
|
||||
val code: String = "",
|
||||
val isButtonEnabled: Boolean = false,
|
||||
val isErrorVisible: Boolean = false,
|
||||
) : AuthState
|
||||
}
|
||||
@@ -12,32 +12,83 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||
import ru.myitschool.work.domain.auth.GetSavedAuthCodeUseCase
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||
|
||||
private val checkAndSaveAuthCodeUseCase by lazy {
|
||||
CheckAndSaveAuthCodeUseCase(AuthRepository)
|
||||
}
|
||||
|
||||
private val getSavedAuthCodeUseCase by lazy {
|
||||
GetSavedAuthCodeUseCase(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
|
||||
|
||||
init {
|
||||
// Если код уже сохранён — сразу уходим на главный экран
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val saved = getSavedAuthCodeUseCase()
|
||||
if (saved != null) {
|
||||
_actionFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
is AuthIntent.TextInput -> {
|
||||
val newCode = intent.text
|
||||
val isValid = isValidCode(newCode)
|
||||
_uiState.update {
|
||||
AuthState.Data(
|
||||
code = newCode,
|
||||
isButtonEnabled = isValid,
|
||||
isErrorVisible = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AuthIntent.Send -> {
|
||||
val currentState = _uiState.value
|
||||
if (currentState !is AuthState.Data) return
|
||||
|
||||
val code = currentState.code
|
||||
if (!isValidCode(code)) return
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
_uiState.update { AuthState.Loading }
|
||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
||||
checkAndSaveAuthCodeUseCase.invoke(code).fold(
|
||||
onSuccess = {
|
||||
// успешная авторизация → навигация на главный экран
|
||||
_actionFlow.emit(Unit)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
_actionFlow.emit(Unit)
|
||||
_uiState.update {
|
||||
AuthState.Data(
|
||||
code = code,
|
||||
isButtonEnabled = true,
|
||||
isErrorVisible = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AuthIntent.TextInput -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidCode(text: String): Boolean {
|
||||
if (text.length != 4) return false
|
||||
return text.all { ch ->
|
||||
ch.isDigit() || (ch in 'a'..'z') || (ch in 'A'..'Z')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookIntent {
|
||||
data object Load : BookIntent
|
||||
data object Refresh : BookIntent
|
||||
data class SelectDate(val index: Int) : BookIntent
|
||||
data class SelectPlace(val id: Int) : BookIntent
|
||||
data object Book : BookIntent
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
data class BookPlaceItem(
|
||||
val id: Int,
|
||||
val roomName: String,
|
||||
val time: String,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
|
||||
sealed interface BookState {
|
||||
object Loading : BookState
|
||||
|
||||
data class Data(
|
||||
val dates: List<String>,
|
||||
val selectedDateIndex: Int,
|
||||
val places: List<BookPlaceItem>,
|
||||
) : BookState
|
||||
|
||||
data class Error(
|
||||
val message: String,
|
||||
) : BookState
|
||||
|
||||
/** Нет доступных дат — показываем "Всё забронировано" */
|
||||
object Empty : BookState
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.time.LocalDate
|
||||
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.launch
|
||||
import ru.myitschool.work.core.DateUtils
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
import ru.myitschool.work.domain.booking.BookPlaceUseCase
|
||||
import ru.myitschool.work.domain.booking.GetAvailableBookingsUseCase
|
||||
import ru.myitschool.work.domain.entities.BookingEntity
|
||||
|
||||
class BookViewModel : ViewModel() {
|
||||
|
||||
private val getAvailableBookingsUseCase by lazy {
|
||||
GetAvailableBookingsUseCase(UserRepository)
|
||||
}
|
||||
private val bookPlaceUseCase by lazy {
|
||||
BookPlaceUseCase(UserRepository)
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
|
||||
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
|
||||
|
||||
sealed interface Action {
|
||||
data object CloseWithSuccess : Action
|
||||
}
|
||||
|
||||
private val _actionFlow = MutableSharedFlow<Action>()
|
||||
val actionFlow: SharedFlow<Action> = _actionFlow
|
||||
|
||||
private var allGroups: List<DateGroup> = emptyList()
|
||||
private var selectedDateIndex: Int = 0
|
||||
private var selectedPlaceId: Int? = null
|
||||
|
||||
fun onIntent(intent: BookIntent) {
|
||||
when (intent) {
|
||||
BookIntent.Load,
|
||||
BookIntent.Refresh -> {
|
||||
load()
|
||||
}
|
||||
|
||||
is BookIntent.SelectDate -> {
|
||||
selectDate(intent.index)
|
||||
}
|
||||
|
||||
is BookIntent.SelectPlace -> {
|
||||
selectedPlaceId = intent.id
|
||||
val current = _uiState.value
|
||||
if (current is BookState.Data) {
|
||||
val updatedPlaces = current.places.map { item ->
|
||||
item.copy(isSelected = item.id == intent.id)
|
||||
}
|
||||
_uiState.value = current.copy(places = updatedPlaces)
|
||||
}
|
||||
}
|
||||
|
||||
BookIntent.Book -> {
|
||||
book()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
_uiState.value = BookState.Loading
|
||||
|
||||
getAvailableBookingsUseCase()
|
||||
.fold(
|
||||
onSuccess = { bookings ->
|
||||
if (bookings.isEmpty()) {
|
||||
_uiState.value = BookState.Empty
|
||||
allGroups = emptyList()
|
||||
selectedPlaceId = null
|
||||
selectedDateIndex = 0
|
||||
return@fold
|
||||
}
|
||||
|
||||
allGroups = groupByDate(bookings)
|
||||
|
||||
if (allGroups.isEmpty()) {
|
||||
_uiState.value = BookState.Empty
|
||||
selectedPlaceId = null
|
||||
selectedDateIndex = 0
|
||||
return@fold
|
||||
}
|
||||
|
||||
selectedDateIndex = 0
|
||||
selectedPlaceId = null
|
||||
val firstGroup = allGroups[0]
|
||||
|
||||
val places = firstGroup.slots.mapIndexed { index, slot ->
|
||||
BookPlaceItem(
|
||||
id = index,
|
||||
roomName = slot.roomName,
|
||||
time = slot.time,
|
||||
isSelected = false,
|
||||
)
|
||||
}
|
||||
|
||||
_uiState.value = BookState.Data(
|
||||
dates = allGroups.map { it.label },
|
||||
selectedDateIndex = 0,
|
||||
places = places,
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.value = BookState.Error(
|
||||
message = error.message ?: "Ошибка загрузки бронирований",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectDate(index: Int) {
|
||||
val groups = allGroups
|
||||
if (index !in groups.indices) return
|
||||
|
||||
selectedDateIndex = index
|
||||
selectedPlaceId = null
|
||||
|
||||
val group = groups[index]
|
||||
val places = group.slots.mapIndexed { idx, slot ->
|
||||
BookPlaceItem(
|
||||
id = idx,
|
||||
roomName = slot.roomName,
|
||||
time = slot.time,
|
||||
isSelected = false,
|
||||
)
|
||||
}
|
||||
|
||||
val datesLabels = groups.map { it.label }
|
||||
|
||||
val current = _uiState.value
|
||||
_uiState.value = if (current is BookState.Data) {
|
||||
current.copy(
|
||||
dates = datesLabels,
|
||||
selectedDateIndex = index,
|
||||
places = places,
|
||||
)
|
||||
} else {
|
||||
BookState.Data(
|
||||
dates = datesLabels,
|
||||
selectedDateIndex = index,
|
||||
places = places,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun book() {
|
||||
val current = _uiState.value
|
||||
if (current !is BookState.Data) return
|
||||
|
||||
val placeId = selectedPlaceId ?: return
|
||||
val place = current.places.firstOrNull { it.id == placeId } ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
bookPlaceUseCase(place.roomName, place.time)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(Action.CloseWithSuccess)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.value = BookState.Error(
|
||||
message = error.message ?: "Ошибка бронирования",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DateGroup(
|
||||
val date: LocalDate,
|
||||
val label: String,
|
||||
val slots: List<BookingEntity>,
|
||||
)
|
||||
|
||||
private fun groupByDate(bookings: List<BookingEntity>): List<DateGroup> {
|
||||
val grouped: Map<LocalDate, List<BookingEntity>> =
|
||||
bookings
|
||||
.mapNotNull { booking ->
|
||||
val date = DateUtils.parseDate(booking.time) ?: return@mapNotNull null
|
||||
date to booking
|
||||
}
|
||||
.groupBy({ it.first }, { it.second })
|
||||
|
||||
return grouped.entries
|
||||
.sortedBy { it.key }
|
||||
.map { (date, slots) ->
|
||||
DateGroup(
|
||||
date = date,
|
||||
label = DateUtils.formatForBook(date.toString()),
|
||||
slots = slots,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
sealed interface MainIntent {
|
||||
data object Load : MainIntent
|
||||
data object Refresh : MainIntent
|
||||
data object Logout : MainIntent
|
||||
data object AddBooking : MainIntent
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
data class MainBookingItem(
|
||||
val id: Int,
|
||||
val dateLabel: String,
|
||||
val roomName: String,
|
||||
)
|
||||
|
||||
sealed interface MainState {
|
||||
object Loading : MainState
|
||||
|
||||
data class Data(
|
||||
val name: String,
|
||||
val bookings: List<MainBookingItem>,
|
||||
) : MainState
|
||||
|
||||
data class Error(
|
||||
val message: String,
|
||||
) : MainState
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.time.LocalDate
|
||||
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.launch
|
||||
import ru.myitschool.work.core.DateUtils
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
import ru.myitschool.work.domain.auth.ClearAuthDataUseCase
|
||||
import ru.myitschool.work.domain.user.GetUserInfoUseCase
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
||||
private val getUserInfoUseCase by lazy { GetUserInfoUseCase(UserRepository) }
|
||||
private val clearAuthDataUseCase by lazy { ClearAuthDataUseCase(AuthRepository) }
|
||||
|
||||
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||
|
||||
sealed interface Action {
|
||||
data object NavigateToAuth : Action
|
||||
data object NavigateToBooking : Action
|
||||
}
|
||||
|
||||
private val _actionFlow = MutableSharedFlow<Action>()
|
||||
val actionFlow: SharedFlow<Action> = _actionFlow
|
||||
|
||||
fun onIntent(intent: MainIntent) {
|
||||
when (intent) {
|
||||
MainIntent.Load,
|
||||
MainIntent.Refresh -> {
|
||||
load()
|
||||
}
|
||||
|
||||
MainIntent.Logout -> {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
clearAuthDataUseCase()
|
||||
_actionFlow.emit(Action.NavigateToAuth)
|
||||
}
|
||||
}
|
||||
|
||||
MainIntent.AddBooking -> {
|
||||
viewModelScope.launch {
|
||||
_actionFlow.emit(Action.NavigateToBooking)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
_uiState.value = MainState.Loading
|
||||
getUserInfoUseCase()
|
||||
.fold(
|
||||
onSuccess = { user ->
|
||||
val bookings = user.bookings
|
||||
.sortedWith(
|
||||
compareBy { booking ->
|
||||
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||
}
|
||||
)
|
||||
.mapIndexed { index, booking ->
|
||||
MainBookingItem(
|
||||
id = index,
|
||||
dateLabel = DateUtils.formatForMain(booking.time),
|
||||
roomName = booking.roomName,
|
||||
)
|
||||
}
|
||||
|
||||
_uiState.value = MainState.Data(
|
||||
name = user.name,
|
||||
bookings = bookings,
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.value = MainState.Error(
|
||||
message = error.message ?: "Ошибка загрузки данных",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user