Merge pull request 'room booking' (#10) from room_board into main

Reviewed-on: Minipigi-org/NTO-2026-Android-TeamTask-Template#10
This commit is contained in:
student-d-sherstnev 2026-02-25 13:58:43 +00:00
commit 6fecb0890b
22 changed files with 288 additions and 18 deletions

View File

@ -1,10 +1,11 @@
package ru.myitschool.work.core
object Constants {
const val HOST = "http://10.0.0.103:49165"
const val HOST = "http://10.0.0.12:49165"
const val AUTH_URL = "/login"
const val INFO_URL = "/info"
const val BOOKING_URL = "/booking"
const val BOOK_URL = "/book"
const val ROOM_BOOKING_URL = "/room/booking"
const val AUTH_DELAY = 60
}

View File

@ -17,4 +17,6 @@ data class AuthResponseDto(
val token: String,
@SerialName("expired")
val expired: Int,
@SerialName("role")
val role: String
)

View File

@ -8,5 +8,5 @@ data class BookRequestDto(
@SerialName("date")
val date: String,
@SerialName("placeId")
val placeId: String,
val placeId: Int,
)

View File

@ -0,0 +1,12 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RoomBookingRequestDto(
@SerialName("roomId")
val roomId: Int,
@SerialName("date")
val date: String,
)

View File

@ -16,8 +16,12 @@ import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository {
private const val STORE = "AUTH-STORE"
private const val TOKEN_KEY = "TOKEN"
private const val ROLE_KEY = "ROLE"
private var tokenCache: String? = null
private var roleCache: String? = null
suspend fun checkAndSave(login: String, password: String): Result<AuthResponseDto> {
val data = AuthRequestDto(login=login, password=password)
@ -29,6 +33,10 @@ object AuthRepository {
val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences[prefKey] = encryptedTokenCache
}
App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(ROLE_KEY)
preferences[prefKey] = success.role
}
}
}
}
@ -47,12 +55,27 @@ object AuthRepository {
return null
}
suspend fun getRole(): String? {
if (roleCache == null) {
roleCache = App.context.userDataStore.data
.firstOrNull()
?.let { preferences ->
preferences[stringPreferencesKey(ROLE_KEY)]
}
}
return roleCache
}
suspend fun logout() {
tokenCache = null
App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences.remove(prefKey)
}
App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(ROLE_KEY)
preferences.remove(prefKey)
}
}
private val Context.userDataStore: DataStore<Preferences> by preferencesDataStore(name = STORE)

View File

@ -0,0 +1,32 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.dto.BookRequestDto
import ru.myitschool.work.data.dto.RoomBookingRequestDto
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.room.entities.RoomEntity
class RoomBookingRepository(
private val authRepository: AuthRepository
) {
suspend fun getRoomBookings(): Result<RoomEntity> {
val token = authRepository.getToken() ?: return getNoAuthResult()
return NetworkDataSource.getRoomBookings(token).mapCatching { dto ->
RoomEntity(dto)
}
}
suspend fun deleteRoomBooking(roomId: Int, date: String): Result<Boolean> {
val token = authRepository.getToken() ?: return getNoAuthResult()
val data = RoomBookingRequestDto(roomId = roomId, date = date)
return NetworkDataSource.deleteRoomBooking(token, data)
}
suspend fun sendRoomBooking(roomId: Int, date: String): Result<Boolean> {
val token = authRepository.getToken() ?: return getNoAuthResult()
val dto = BookRequestDto(date, roomId)
return NetworkDataSource.addBook(token, dto)
}
private fun <T> getNoAuthResult() = Result.failure<T>(
IllegalStateException("No auth")
)
}

View File

@ -4,6 +4,7 @@ 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.delete
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.post
@ -21,6 +22,7 @@ import ru.myitschool.work.data.dto.AuthRequestDto
import ru.myitschool.work.data.dto.AuthResponseDto
import ru.myitschool.work.data.dto.PlaceDto
import ru.myitschool.work.data.dto.BookRequestDto
import ru.myitschool.work.data.dto.RoomBookingRequestDto
import ru.myitschool.work.data.dto.UserDto
import ru.myitschool.work.data.repo.AuthRepository
@ -114,5 +116,42 @@ object NetworkDataSource {
}
}
suspend fun getRoomBookings(token: String): Result<Map<String, String?>> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(Constants.ROOM_BOOKING_URL)) {
headers {
append("Authorization", "Bearer $token")
}
}
if (response.status == HttpStatusCode.OK) {
response.body<Map<String, String?>>()
} else {
if (response.status == HttpStatusCode.Unauthorized) {
AuthRepository.logout()
}
error(response.bodyAsText())
}
}
}
suspend fun deleteRoomBooking(token: String, data: RoomBookingRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.delete(getUrl(Constants.ROOM_BOOKING_URL)) {
headers {
append("Authorization", "Bearer $token")
}
setBody(data)
}
if (response.status == HttpStatusCode.OK) {
true
} else {
if (response.status == HttpStatusCode.Unauthorized) {
AuthRepository.logout()
}
error(response.bodyAsText())
}
}
}
private fun getUrl(targetUrl: String) = "${Constants.HOST}/api$targetUrl"
}

View File

@ -3,7 +3,7 @@ package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.dto.AuthResponseDto
import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase(
class CheckAndSaveAuthTokenUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(

View File

@ -1,6 +1,6 @@
package ru.myitschool.work.domain.auth
class CheckCodeFormatUseCase {
class CheckCredsFormatUseCase {
operator fun invoke(
login: String, password: String
): Boolean {

View File

@ -2,7 +2,7 @@ package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class GetCodeUseCase(
class GetTokenUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(): String? {

View File

@ -2,5 +2,5 @@ package ru.myitschool.work.domain.book.entities
data class BookRequestData(
val date: String,
val placeId: String
val placeId: Int
)

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.domain.room
import ru.myitschool.work.data.repo.RoomBookingRepository
class DeleteRoomBookingsUseCase(
private val repository: RoomBookingRepository
) {
suspend operator fun invoke(roomId: Int, date: String): Result<Boolean> {
return repository.deleteRoomBooking(roomId = roomId, date = date)
}
}

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.domain.room
import ru.myitschool.work.data.repo.AuthRepository
class GetRoleUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(): String? {
return repository.getRole()
}
}

View File

@ -0,0 +1,12 @@
package ru.myitschool.work.domain.room
import ru.myitschool.work.data.repo.RoomBookingRepository
import ru.myitschool.work.domain.room.entities.RoomEntity
class GetRoomBookingsUseCase(
private val repository: RoomBookingRepository
) {
suspend operator fun invoke(): Result<RoomEntity> {
return repository.getRoomBookings()
}
}

View File

@ -0,0 +1,13 @@
package ru.myitschool.work.domain.room
import ru.myitschool.work.data.repo.RoomBookingRepository
class SendRoomBookingRequestUseCase(
private val repository: RoomBookingRepository
) {
suspend operator fun invoke(roomId: Int, date: String): Result<Unit> {
return repository.sendRoomBooking(roomId, date).mapCatching { success ->
if (!success) error("Book error")
}
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object RoomScreenDestination: AppDestination

View File

@ -14,14 +14,17 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.GetCodeUseCase
import ru.myitschool.work.domain.auth.GetTokenUseCase
import ru.myitschool.work.domain.room.GetRoleUseCase
import ru.myitschool.work.ui.nav.AppDestination
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.nav.RoomScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
import ru.myitschool.work.ui.screen.room.RoomScreen
@Composable
fun AppNavHost(
@ -30,9 +33,12 @@ fun AppNavHost(
) {
var destination by remember { mutableStateOf<AppDestination?>(null) }
LaunchedEffect(Unit) {
val code = GetCodeUseCase(AuthRepository).invoke()
val code = GetTokenUseCase(AuthRepository).invoke()
val role = GetRoleUseCase(AuthRepository).invoke()
destination = if (code == null) {
AuthScreenDestination
} else if (role == "room") {
RoomScreenDestination
} else {
MainScreenDestination
}
@ -54,6 +60,9 @@ fun AppNavHost(
composable<BookScreenDestination> {
BookScreen(navController = navController)
}
composable<RoomScreenDestination> {
RoomScreen(navController = navController)
}
}
}
}

View File

@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
import ru.myitschool.work.domain.auth.CheckAndSaveAuthTokenUseCase
import ru.myitschool.work.domain.auth.CheckCredsFormatUseCase
import ru.myitschool.work.ui.nav.MainScreenDestination
class AuthViewModel : ViewModel() {
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val checkCodeFormatUseCase by lazy { CheckCredsFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthTokenUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data(
isEnabledSend = false,

View File

@ -4,6 +4,6 @@ sealed interface BookIntent {
data object Refresh: BookIntent
data class Add(
val date: String,
val placeId: String
val placeId: Int
): BookIntent
}

View File

@ -170,7 +170,7 @@ private fun ContentState(
mutableIntStateOf(startDestination.index)
}
var selectedPlaceId by rememberSaveable {
mutableStateOf<String?>(null)
mutableStateOf<Int?>(null)
}
Box {
Column {
@ -213,7 +213,7 @@ private fun ContentState(
startDestination = startDestination,
state = state,
onPlaceSelected = { id ->
selectedPlaceId = id
selectedPlaceId = id.toInt()
}
)
}

View File

@ -2,7 +2,6 @@ package ru.myitschool.work.ui.screen.room
sealed interface RoomIntent {
data object Refresh: RoomIntent
data class Booking(val placeId: Int): RoomIntent
data object Book: RoomIntent
data object UnBook: RoomIntent
data class Booking(val placeId: Int, val date: String): RoomIntent
data class UnBook(val placeId: Int, val date: String): RoomIntent
}

View File

@ -1,7 +1,107 @@
package ru.myitschool.work.ui.screen.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
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.RoomBookingRepository
import ru.myitschool.work.domain.room.DeleteRoomBookingsUseCase
import ru.myitschool.work.domain.room.GetRoomBookingsUseCase
import ru.myitschool.work.domain.room.SendRoomBookingRequestUseCase
class RoomViewModel : ViewModel() {
private val roomBookingRepository = RoomBookingRepository(
AuthRepository
)
private val getRoomBookingsDataUseCase by lazy {
GetRoomBookingsUseCase(
RoomBookingRepository(
AuthRepository
)
)
}
private val deleteRoomBookingRequestUseCase by lazy {
DeleteRoomBookingsUseCase(
roomBookingRepository
)
}
private val sendRoomBookingRequestUseCase by lazy {
SendRoomBookingRequestUseCase(
roomBookingRepository
)
}
private val _uiState = MutableStateFlow<RoomState>(RoomState.Loading)
val uiState: StateFlow<RoomState> = _uiState.asStateFlow()
init {
refresh()
}
fun onIntent(intent: RoomIntent) {
when (intent) {
is RoomIntent.Refresh -> {
refresh()
}
is RoomIntent.Booking -> {
viewModelScope.launch {
sendRoomBookingRequestUseCase.invoke(
intent.placeId,
intent.date
).fold(
onSuccess = {
// _actionFlow.emit(BookAction.BackWithSuccess)
refresh()
},
onFailure = { error ->
error.printStackTrace()
}
)
}
}
is RoomIntent.UnBook -> {
viewModelScope.launch {
deleteRoomBookingRequestUseCase.invoke(
intent.placeId,
intent.date
).fold(
onSuccess = {
// _actionFlow.emit(BookAction.BackWithSuccess)
refresh()
},
onFailure = { error ->
error.printStackTrace()
}
)
}
}
}
}
private fun refresh() {
viewModelScope.launch {
_uiState.update { RoomState.Loading }
_uiState.update {
getRoomBookingsDataUseCase.invoke().fold(
onSuccess = { data ->
RoomState.Data(
data = data
)
},
onFailure = { error ->
RoomState.Error(
error = error.message.orEmpty()
)
}
)
}
}
}
}