new_select_screen #9

Open
student-i-nikolaevskiy wants to merge 42 commits from Minipigi-org/NTO-2026-Android-TeamTask-Template:new_select_screen into main
22 changed files with 288 additions and 18 deletions
Showing only changes of commit 6fecb0890b - Show all commits

View File

@ -1,10 +1,11 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { 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 AUTH_URL = "/login"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" const val BOOKING_URL = "/booking"
const val BOOK_URL = "/book" const val BOOK_URL = "/book"
const val ROOM_BOOKING_URL = "/room/booking"
const val AUTH_DELAY = 60 const val AUTH_DELAY = 60
} }

View File

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

View File

@ -8,5 +8,5 @@ data class BookRequestDto(
@SerialName("date") @SerialName("date")
val date: String, val date: String,
@SerialName("placeId") @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 { object AuthRepository {
private const val STORE = "AUTH-STORE" private const val STORE = "AUTH-STORE"
private const val TOKEN_KEY = "TOKEN" private const val TOKEN_KEY = "TOKEN"
private const val ROLE_KEY = "ROLE"
private var tokenCache: String? = null private var tokenCache: String? = null
private var roleCache: String? = null
suspend fun checkAndSave(login: String, password: String): Result<AuthResponseDto> { suspend fun checkAndSave(login: String, password: String): Result<AuthResponseDto> {
val data = AuthRequestDto(login=login, password=password) val data = AuthRequestDto(login=login, password=password)
@ -29,6 +33,10 @@ object AuthRepository {
val prefKey = stringPreferencesKey(TOKEN_KEY) val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences[prefKey] = encryptedTokenCache 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 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() { suspend fun logout() {
tokenCache = null tokenCache = null
App.context.userDataStore.edit { preferences -> App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(TOKEN_KEY) val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences.remove(prefKey) 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) 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.call.body
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.delete
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.headers import io.ktor.client.request.headers
import io.ktor.client.request.post 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.AuthResponseDto
import ru.myitschool.work.data.dto.PlaceDto import ru.myitschool.work.data.dto.PlaceDto
import ru.myitschool.work.data.dto.BookRequestDto 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.dto.UserDto
import ru.myitschool.work.data.repo.AuthRepository 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" 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.dto.AuthResponseDto
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
class CheckAndSaveAuthCodeUseCase( class CheckAndSaveAuthTokenUseCase(
private val repository: AuthRepository private val repository: AuthRepository
) { ) {
suspend operator fun invoke( suspend operator fun invoke(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,107 @@
package ru.myitschool.work.ui.screen.room package ru.myitschool.work.ui.screen.room
import androidx.lifecycle.ViewModel 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() { 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()
)
}
)
}
}
}
} }