diff --git a/app/src/main/java/ru/myitschool/work/core/DateUtils.kt b/app/src/main/java/ru/myitschool/work/core/DateUtils.kt new file mode 100644 index 0000000..3bc85da --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/DateUtils.kt @@ -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) + } +} diff --git a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt index 3ef28f1..d1dafdd 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/AuthRepository.kt @@ -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 { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text +// suspend fun checkAndSave(text: String): Result { +// return NetworkDataSource.checkAuth(text).onSuccess { success -> +// if (success) { +// codeCache = text +// } +// } +// } +suspend fun checkAndSave(text: String): Result { + 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() + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt new file mode 100644 index 0000000..2f4faa5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/UserRepository.kt @@ -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 { + 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 { booking -> + DateUtils.parseDate(booking.time) ?: LocalDate.MAX + }.thenBy { it.time } + ) + + UserEntity( + name = dto.name, + bookings = bookings, + ) + } + } + + suspend fun getAvailableBookings(): Result> { + 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 { + 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, + ) +} diff --git a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt index fbdfef5..33c3a8e 100644 --- a/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/source/NetworkDataSource.kt @@ -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 = withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.INFO_URL)) + when (response.status) { + HttpStatusCode.OK -> response.body() + else -> error(response.bodyAsText()) + } + } + } + + suspend fun getAvailableBookings( + code: String + ): Result> = withContext(Dispatchers.IO) { + runCatching { + val response = client.get(getUrl(code, Constants.BOOKING_URL)) + when (response.status) { + HttpStatusCode.OK -> response.body>() + else -> error(response.bodyAsText()) + } + } + } + + suspend fun book( + code: String, + room: String, + time: String + ): Result = 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" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/source/dto/UserDto.kt b/app/src/main/java/ru/myitschool/work/data/source/dto/UserDto.kt new file mode 100644 index 0000000..aa5a093 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/source/dto/UserDto.kt @@ -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 +) { + @Serializable + data class BookingDto( + @SerialName("room") + val room: String, + @SerialName("time") + val time: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/ClearAuthDataUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/ClearAuthDataUseCase.kt new file mode 100644 index 0000000..a53ba05 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/ClearAuthDataUseCase.kt @@ -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() + } +} diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetSavedAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetSavedAuthCodeUseCase.kt new file mode 100644 index 0000000..c60adf5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetSavedAuthCodeUseCase.kt @@ -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() + } +} diff --git a/app/src/main/java/ru/myitschool/work/domain/booking/BookPlaceUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/booking/BookPlaceUseCase.kt new file mode 100644 index 0000000..bfc5787 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/booking/BookPlaceUseCase.kt @@ -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 { + return repository.book(room, time) + } +} diff --git a/app/src/main/java/ru/myitschool/work/domain/booking/GetAvailableBookingsUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/booking/GetAvailableBookingsUseCase.kt new file mode 100644 index 0000000..1ee8ee2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/booking/GetAvailableBookingsUseCase.kt @@ -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> { + return repository.getAvailableBookings() + } +} diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt new file mode 100644 index 0000000..0bf603d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/BookingEntity.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.domain.entities + +data class BookingEntity( + val roomName: String, + val time: String, +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt new file mode 100644 index 0000000..7aba90e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.domain.entities + +data class UserEntity( + val name: String, + val bookings: List, +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/user/GetAvailableBookingsUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/user/GetAvailableBookingsUseCase.kt new file mode 100644 index 0000000..c20101c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/user/GetAvailableBookingsUseCase.kt @@ -0,0 +1,4 @@ +package ru.myitschool.work.domain.user + +class GetAvailableBookingsUseCase { +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/user/GetUserInfoUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/user/GetUserInfoUseCase.kt new file mode 100644 index 0000000..1e9675e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/user/GetUserInfoUseCase.kt @@ -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 { + return repository.getUserInfo() + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt index a06ba76..e65acf9 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthState.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt index 3153640..706b9b7 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthViewModel.kt @@ -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.Data) + + private val checkAndSaveAuthCodeUseCase by lazy { + CheckAndSaveAuthCodeUseCase(AuthRepository) + } + + private val getSavedAuthCodeUseCase by lazy { + GetSavedAuthCodeUseCase(AuthRepository) + } + + private val _uiState = MutableStateFlow(AuthState.Data()) val uiState: StateFlow = _uiState.asStateFlow() + // единичное событие навигации на главный экран private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _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 } } -} \ No newline at end of file + + 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') + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt new file mode 100644 index 0000000..db58a3b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookIntent.kt @@ -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 +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt new file mode 100644 index 0000000..ccc83df --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookState.kt @@ -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, + val selectedDateIndex: Int, + val places: List, + ) : BookState + + data class Error( + val message: String, + ) : BookState + + /** Нет доступных дат — показываем "Всё забронировано" */ + object Empty : BookState +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt new file mode 100644 index 0000000..5c517f3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + sealed interface Action { + data object CloseWithSuccess : Action + } + + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _actionFlow + + private var allGroups: List = 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, + ) + + private fun groupByDate(bookings: List): List { + val grouped: Map> = + 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, + ) + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt new file mode 100644 index 0000000..e5fb13d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainIntent.kt @@ -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 +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt new file mode 100644 index 0000000..5b2513f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainState.kt @@ -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, + ) : MainState + + data class Error( + val message: String, + ) : MainState +} diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt new file mode 100644 index 0000000..6e7a180 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/main/MainViewModel.kt @@ -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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + sealed interface Action { + data object NavigateToAuth : Action + data object NavigateToBooking : Action + } + + private val _actionFlow = MutableSharedFlow() + val actionFlow: SharedFlow = _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 ?: "Ошибка загрузки данных", + ) + } + ) + } + } +}