diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..87339d2 --- /dev/null +++ b/README.MD @@ -0,0 +1,3 @@ +penpot deck: https://pp.sicampus.ru/#/workspace?team-id=14a6b474-d5fa-807f-8007-9f0741c1b7ad&project-id=14a6b474-d5fa-807f-8007-9f0741c1b7ae&file-id=8e3f3321-7df1-807a-8007-9f6a7201af74&page-id=50b061ab-362d-8056-8007-9d94752aa99f&layout=layers&board-id=6fa8b836-315d-80ad-8007-9f6ddff01da2 +login: anna +password: qwerty1! \ No newline at end of file diff --git a/S-APP-MINIPIGS.penpot b/S-APP-MINIPIGS.penpot new file mode 100644 index 0000000..cf2e9a7 Binary files /dev/null and b/S-APP-MINIPIGS.penpot differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2c02bd..a5c8fbd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,8 +17,9 @@ tools:targetApi="31"> diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt index cd36239..6121f27 100644 --- a/app/src/main/java/ru/myitschool/work/core/Constants.kt +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -1,9 +1,11 @@ package ru.myitschool.work.core object Constants { - const val HOST = "http://localhost:8090" - const val AUTH_URL = "/auth" + 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 } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/AESEncryption.kt b/app/src/main/java/ru/myitschool/work/data/AESEncryption.kt new file mode 100644 index 0000000..00e2a8b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/AESEncryption.kt @@ -0,0 +1,58 @@ +package ru.myitschool.work.data + +import android.util.Base64 +import javax.crypto.Cipher +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + + +object AESEncyption { + + const val secretKey = "tK5Ugskdkipokuodvknfdk3434weofnf=" + const val salt = "QLlGNHNhYTJTQWZ2bGhpV3U=" + const val iv = "bVQqNFNhRkQ1Njc4UUFaPA==" + + fun encrypt(strToEncrypt: String): String? { + try { + val ivParameterSpec = IvParameterSpec(Base64.decode(iv, Base64.DEFAULT)) + + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + val spec = + PBEKeySpec(secretKey.toCharArray(), Base64.decode(salt, Base64.DEFAULT), 10000, 256) + val tmp = factory.generateSecret(spec) + val secretKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec) + return Base64.encodeToString( + cipher.doFinal(strToEncrypt.toByteArray(Charsets.UTF_8)), + Base64.DEFAULT + ) + } catch (e: Exception) { + println("Error while encrypting: $e") + } + return null + } + + fun decrypt(strToDecrypt: String?): String? { + try { + + val ivParameterSpec = IvParameterSpec(Base64.decode(iv, Base64.DEFAULT)) + + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + val spec = + PBEKeySpec(secretKey.toCharArray(), Base64.decode(salt, Base64.DEFAULT), 10000, 256) + val tmp = factory.generateSecret(spec) + val secretKey = SecretKeySpec(tmp.encoded, "AES") + + val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec) + return String(cipher.doFinal(Base64.decode(strToDecrypt, Base64.DEFAULT))) + } catch (e: Exception) { + println("Error while decrypting: $e") + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/AuthRequestDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/AuthRequestDto.kt new file mode 100644 index 0000000..4481356 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/AuthRequestDto.kt @@ -0,0 +1,22 @@ +package ru.myitschool.work.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AuthRequestDto( + @SerialName("login") + val login: String, + @SerialName("password") + val password: String, +) + +@Serializable +data class AuthResponseDto( + @SerialName("token") + val token: String, + @SerialName("expired") + val expired: Int, + @SerialName("role") + val role: String +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/BookRequestDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/BookRequestDto.kt index 759ffc5..0aae4be 100644 --- a/app/src/main/java/ru/myitschool/work/data/dto/BookRequestDto.kt +++ b/app/src/main/java/ru/myitschool/work/data/dto/BookRequestDto.kt @@ -8,5 +8,5 @@ data class BookRequestDto( @SerialName("date") val date: String, @SerialName("placeId") - val placeId: String, + val placeId: Int, ) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingRequestDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingRequestDto.kt new file mode 100644 index 0000000..d7bfe63 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingRequestDto.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingsDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingsDto.kt new file mode 100644 index 0000000..ece973b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/RoomBookingsDto.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomBookingsDto( + @SerialName("name") + val name: String, + @SerialName("data") + val data: Map +) 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 e4126dd..0f07993 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 @@ -8,41 +8,72 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.firstOrNull import ru.myitschool.work.App +import ru.myitschool.work.data.AESEncyption +import ru.myitschool.work.data.dto.AuthRequestDto +import ru.myitschool.work.data.dto.AuthResponseDto import ru.myitschool.work.data.source.NetworkDataSource object AuthRepository { private const val STORE = "AUTH-STORE" - private const val CODE_KEY = "CODE" + private const val TOKEN_KEY = "TOKEN" + private const val ROLE_KEY = "ROLE" - private var codeCache: String? = null - suspend fun checkAndSave(text: String): Result { - return NetworkDataSource.checkAuth(text).onSuccess { success -> - if (success) { - codeCache = text + private var tokenCache: String? = null + private var roleCache: String? = null + + + suspend fun checkAndSave(login: String, password: String): Result { + val data = AuthRequestDto(login=login, password=password) + return NetworkDataSource.checkAuth(data).onSuccess { success -> + val encryptedTokenCache = AESEncyption.encrypt(success.token) + tokenCache = encryptedTokenCache + if (encryptedTokenCache != null) { App.context.userDataStore.edit { preferences -> - val prefKey = stringPreferencesKey(CODE_KEY) - preferences[prefKey] = text + val prefKey = stringPreferencesKey(TOKEN_KEY) + preferences[prefKey] = encryptedTokenCache + } + App.context.userDataStore.edit { preferences -> + val prefKey = stringPreferencesKey(ROLE_KEY) + preferences[prefKey] = success.role } } } } - suspend fun getCode(): String? { - if (codeCache == null) { - codeCache = App.context.userDataStore.data + suspend fun getToken(): String? { + if (tokenCache == null) { + tokenCache = App.context.userDataStore.data .firstOrNull() ?.let { preferences -> - preferences[stringPreferencesKey(CODE_KEY)] + preferences[stringPreferencesKey(TOKEN_KEY)] } } - return codeCache + if (tokenCache != null) { + return AESEncyption.decrypt(tokenCache) + } + 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() { - codeCache = null + tokenCache = null App.context.userDataStore.edit { preferences -> - val prefKey = stringPreferencesKey(CODE_KEY) + val prefKey = stringPreferencesKey(TOKEN_KEY) + preferences.remove(prefKey) + } + App.context.userDataStore.edit { preferences -> + val prefKey = stringPreferencesKey(ROLE_KEY) preferences.remove(prefKey) } } diff --git a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt index ea1c581..f60d556 100644 --- a/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt +++ b/app/src/main/java/ru/myitschool/work/data/repo/BookRepository.kt @@ -10,8 +10,8 @@ class BookRepository( private val authRepository: AuthRepository ) { suspend fun getInfo(): Result { - val code = authRepository.getCode() ?: return getNoAuthResult() - return NetworkDataSource.getInfo(code).mapCatching { dto -> + val token = authRepository.getToken() ?: return getNoAuthResult() + return NetworkDataSource.getInfo(token).mapCatching { dto -> MainInfoEntity( name = dto.name ?: error("Name is null"), photoUrl = dto.photoUrl ?: error("Photo url is null"), @@ -26,8 +26,8 @@ class BookRepository( } suspend fun getBookingInfo(): Result> { - val code = authRepository.getCode() ?: return getNoAuthResult() - return NetworkDataSource.getBooking(code).mapCatching { dto -> + val token = authRepository.getToken() ?: return getNoAuthResult() + return NetworkDataSource.getBooking(token).mapCatching { dto -> dto?.map { (date, places) -> BookingData( date = date, @@ -43,9 +43,9 @@ class BookRepository( } suspend fun sendBook(data: BookRequestData): Result { - val code = authRepository.getCode() ?: return getNoAuthResult() + val token = authRepository.getToken() ?: return getNoAuthResult() val dto = BookRequestDto(data.date, data.placeId) - return NetworkDataSource.addBook(code, dto) + return NetworkDataSource.addBook(token, dto) } private fun getNoAuthResult() = Result.failure( IllegalStateException("No auth") diff --git a/app/src/main/java/ru/myitschool/work/data/repo/RoomBookingRepository.kt b/app/src/main/java/ru/myitschool/work/data/repo/RoomBookingRepository.kt new file mode 100644 index 0000000..1a38005 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/repo/RoomBookingRepository.kt @@ -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(roomId: Int): Result { + val token = authRepository.getToken() ?: return getNoAuthResult() + return NetworkDataSource.getRoomBookings(token, roomId = roomId).mapCatching { dto -> + RoomEntity(name=dto.name, data=dto.data) + } + } + + suspend fun deleteRoomBooking(roomId: Int, date: String): Result { + 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 { + val token = authRepository.getToken() ?: return getNoAuthResult() + val dto = BookRequestDto(date, roomId) + return NetworkDataSource.addBook(token, dto) + } + private fun getNoAuthResult() = Result.failure( + IllegalStateException("No auth") + ) +} \ No newline at end of file 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 85387ac..5473780 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 @@ -4,7 +4,9 @@ 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 import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText @@ -16,9 +18,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import ru.myitschool.work.core.Constants +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.RoomBookingsDto import ru.myitschool.work.data.dto.UserDto +import ru.myitschool.work.data.repo.AuthRepository +import ru.myitschool.work.domain.room.entities.RoomEntity object NetworkDataSource { private val client by lazy { @@ -36,54 +44,116 @@ object NetworkDataSource { } } - suspend fun checkAuth(code: String): Result = withContext(Dispatchers.IO) { + suspend fun checkAuth(data: AuthRequestDto): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.AUTH_URL)) + val response = client.post(getUrl(Constants.AUTH_URL)) { + contentType(ContentType.Application.Json) + setBody(data) + } when (response.status) { - HttpStatusCode.OK -> true - else -> false + HttpStatusCode.OK -> response.body() + else -> error(response.bodyAsText()) } } } - suspend fun getInfo(code: String): Result = withContext(Dispatchers.IO) { + suspend fun getInfo(token: String): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - println("!!!!!!!!!!!!!! getInfo $code") - val response = client.get(getUrl(code, Constants.INFO_URL)) + println("!!!!!!!!!!!!!! getInfo") + val response = client.get(getUrl(Constants.INFO_URL)) { + headers { + append("Authorization", "Bearer $token") + } + } if (response.status == HttpStatusCode.OK) { println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}") response.body() } else { + if (response.status == HttpStatusCode.Unauthorized) { + AuthRepository.logout() + } println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}") error(response.bodyAsText()) } } } - suspend fun getBooking(code: String): Result>?> = withContext(Dispatchers.IO) { + suspend fun getBooking(token: String): Result>?> = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.get(getUrl(code, Constants.BOOKING_URL)) + val response = client.get(getUrl(Constants.BOOKING_URL)) { + headers { + append("Authorization", "Bearer $token") + } + } if (response.status == HttpStatusCode.OK) { response.body>>() } else { + if (response.status == HttpStatusCode.Unauthorized) { + AuthRepository.logout() + } error(response.bodyAsText()) } } } - suspend fun addBook(code: String, data: BookRequestDto): Result = withContext(Dispatchers.IO) { + suspend fun addBook(token: String, data: BookRequestDto): Result = withContext(Dispatchers.IO) { return@withContext runCatching { - val response = client.post(getUrl(code, Constants.BOOK_URL)) { + val response = client.post(getUrl(Constants.BOOK_URL)) { contentType(ContentType.Application.Json) setBody(data) + headers { + append("Authorization", "Bearer $token") + } } when (response.status) { HttpStatusCode.Created -> true HttpStatusCode.Conflict -> false - else -> error(response.bodyAsText()) + else -> { + if (response.status == HttpStatusCode.Unauthorized) { + AuthRepository.logout() + } + error(response.bodyAsText()) + } } } } - private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" + suspend fun getRoomBookings(token: String, roomId: Int): Result = withContext(Dispatchers.IO) { + return@withContext runCatching { + val response = client.get(getUrl("$Constants.ROOM_BOOKING_URL/$roomId")) { + headers { + append("Authorization", "Bearer $token") + } + } + if (response.status == HttpStatusCode.OK) { + response.body() + } else { + if (response.status == HttpStatusCode.Unauthorized) { + AuthRepository.logout() + } + error(response.bodyAsText()) + } + } + } + + suspend fun deleteRoomBooking(token: String, data: RoomBookingRequestDto): Result = 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" } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt deleted file mode 100644 index 012fb6f..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthCodeUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ru.myitschool.work.domain.auth - -import ru.myitschool.work.data.repo.AuthRepository - -class CheckAndSaveAuthCodeUseCase( - private val repository: AuthRepository -) { - suspend operator fun invoke( - text: String - ): Result { - return repository.checkAndSave(text).mapCatching { success -> - if (!success) error("Code is incorrect") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthTokenUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthTokenUseCase.kt new file mode 100644 index 0000000..fec73db --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckAndSaveAuthTokenUseCase.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.domain.auth + +import ru.myitschool.work.data.dto.AuthResponseDto +import ru.myitschool.work.data.repo.AuthRepository + +class CheckAndSaveAuthTokenUseCase( + private val repository: AuthRepository +) { + suspend operator fun invoke( + login: String, + password: String + ): Result { + return repository.checkAndSave(login, password) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt deleted file mode 100644 index fe291a0..0000000 --- a/app/src/main/java/ru/myitschool/work/domain/auth/CheckCodeFormatUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.myitschool.work.domain.auth - -class CheckCodeFormatUseCase { - operator fun invoke( - text: String - ): Boolean { - return text.length == 4 && text.all { char -> - char.isLetterOrDigit() && - ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit()) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/CheckCredsFormatUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/CheckCredsFormatUseCase.kt new file mode 100644 index 0000000..3a7b743 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/auth/CheckCredsFormatUseCase.kt @@ -0,0 +1,19 @@ +package ru.myitschool.work.domain.auth + +class CheckCredsFormatUseCase { + operator fun invoke( + login: String, password: String + ): Boolean { + val passwordList = password.toList() + var passwordCorrect = true + for (i in 1..(passwordList.size - 3)) { + val s = StringBuilder().append(passwordList[i]).append(passwordList[i+1]).append(passwordList[i+2]).toString() + if (login.contains(s)) { + passwordCorrect = false + } + } + return login.isNotEmpty() && password.length >= 8 && passwordCorrect && login.all { char -> + char.isLetterOrDigit() && ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit()) + } && password.all({ char -> password.count { it == char } < 3 }) && password.count { it == '!' } + password.count { it == '@' } + password.count { it == '#' } + password.count { it == '"' } + password.count { it == '№' } + password.count { it == ';' } + password.count { it == '$' } + password.count { it == '%' } + password.count { it == '^' } + password.count { it == ':' } + password.count { it == '&' } + password.count { it == '?' } + password.count { it == '*' } >= 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenUseCase.kt similarity index 76% rename from app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt rename to app/src/main/java/ru/myitschool/work/domain/auth/GetTokenUseCase.kt index a3c22b8..2a5b766 100644 --- a/app/src/main/java/ru/myitschool/work/domain/auth/GetCodeUseCase.kt +++ b/app/src/main/java/ru/myitschool/work/domain/auth/GetTokenUseCase.kt @@ -2,10 +2,10 @@ 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? { - return repository.getCode() + return repository.getToken() } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/book/entities/BookRequestData.kt b/app/src/main/java/ru/myitschool/work/domain/book/entities/BookRequestData.kt index 431a9ad..b39bb37 100644 --- a/app/src/main/java/ru/myitschool/work/domain/book/entities/BookRequestData.kt +++ b/app/src/main/java/ru/myitschool/work/domain/book/entities/BookRequestData.kt @@ -2,5 +2,5 @@ package ru.myitschool.work.domain.book.entities data class BookRequestData( val date: String, - val placeId: String + val placeId: Int ) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/room/DeleteRoomBookingsUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/room/DeleteRoomBookingsUseCase.kt new file mode 100644 index 0000000..6cd5faa --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/room/DeleteRoomBookingsUseCase.kt @@ -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 { + return repository.deleteRoomBooking(roomId = roomId, date = date) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/room/GetRoleUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/room/GetRoleUseCase.kt new file mode 100644 index 0000000..43e9b9a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/room/GetRoleUseCase.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/room/GetRoomBookingsUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/room/GetRoomBookingsUseCase.kt new file mode 100644 index 0000000..e959870 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/room/GetRoomBookingsUseCase.kt @@ -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(roomId: Int): Result { + return repository.getRoomBookings(roomId = roomId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/room/SendRoomBookingUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/room/SendRoomBookingUseCase.kt new file mode 100644 index 0000000..d50e71c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/room/SendRoomBookingUseCase.kt @@ -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 { + return repository.sendRoomBooking(roomId, date).mapCatching { success -> + if (!success) error("Book error") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/room/entities/RoomEntity.kt b/app/src/main/java/ru/myitschool/work/domain/room/entities/RoomEntity.kt new file mode 100644 index 0000000..b41a65b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/room/entities/RoomEntity.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.domain.room.entities + + +class RoomEntity ( + val name: String, + val data: Map +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/nav/RoomScreenDestination.kt b/app/src/main/java/ru/myitschool/work/ui/nav/RoomScreenDestination.kt new file mode 100644 index 0000000..0a4a1b3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/nav/RoomScreenDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.nav + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomScreenDestination(val roomId: Int): AppDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt index 54b156d..82b09f5 100644 --- a/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.ui.root import android.os.Bundle +import android.view.WindowManager.LayoutParams import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -15,6 +16,8 @@ class RootActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + actionBar?.hide() + window.setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE) setContent { WorkTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt index 3590d24..94075b5 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/NavigationGraph.kt @@ -10,19 +10,24 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.delay +import androidx.navigation.navArgument +import androidx.navigation.toRoute 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( @@ -31,12 +36,15 @@ fun AppNavHost( ) { var destination by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - val code = GetCodeUseCase(AuthRepository).invoke() - destination = if (code == null) { + val code = GetTokenUseCase(AuthRepository).invoke() + val role = GetRoleUseCase(AuthRepository).invoke() + destination = (if (code == null) { AuthScreenDestination + } else if (role == "ROOM") { + RoomScreenDestination } else { MainScreenDestination - } + }) as AppDestination? } if (destination != null) { NavHost( @@ -55,6 +63,13 @@ fun AppNavHost( composable { BookScreen(navController = navController) } + composable { backStackEntry -> + val room: RoomScreenDestination = backStackEntry.toRoute() + RoomScreen( + roomId = room.roomId, + navController = navController + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt index 74f200a..b4aec57 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthIntent.kt @@ -1,6 +1,6 @@ package ru.myitschool.work.ui.screen.auth sealed interface AuthIntent { - data class Send(val text: String): AuthIntent - data class TextInput(val text: String): AuthIntent + data class Send(val login: String, val password: String): AuthIntent + data class TextInput(val login: String, val password: String): AuthIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt index 4b91b98..8225762 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/auth/AuthScreen.kt @@ -1,17 +1,33 @@ package ru.myitschool.work.ui.screen.auth +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imeNestedScroll +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -21,8 +37,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -31,12 +47,14 @@ import androidx.navigation.NavController import ru.myitschool.work.R import ru.myitschool.work.core.TestIds +@OptIn(ExperimentalLayoutApi::class) @Composable fun AuthScreen( viewModel: AuthViewModel = viewModel(), navController: NavController ) { val state by viewModel.uiState.collectAsState() + val isIme = WindowInsets.isImeVisible LaunchedEffect(Unit) { viewModel.actionFlow.collect { action -> @@ -48,24 +66,24 @@ fun AuthScreen( } } - Column( - modifier = Modifier - .fillMaxSize() - .padding(all = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.auth_title), - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - when (val currentState = state) { - is AuthState.Data -> Content(viewModel, currentState) - is AuthState.Loading -> { - CircularProgressIndicator( - modifier = Modifier.size(64.dp) - ) + BoxWithConstraints { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = if(maxWidth < 400.dp) 48.dp else 200.dp) + .verticalScroll(rememberScrollState()) + .imePadding() + .imeNestedScroll(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + when (val currentState = state) { + is AuthState.Data -> Content(viewModel, currentState, isIme) + is AuthState.Loading -> { + CircularProgressIndicator( + modifier = Modifier.size(64.dp) + ) + } } } } @@ -74,35 +92,90 @@ fun AuthScreen( @Composable private fun Content( viewModel: AuthViewModel, - state: AuthState.Data + state: AuthState.Data, + isIme: Boolean ) { - var inputText by remember { mutableStateOf("") } - Spacer(modifier = Modifier.size(16.dp)) - TextField( + var login by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + + AnimatedVisibility(!isIme ) { + Icon( + painter = painterResource(R.drawable.difference), + contentDescription = stringResource(R.string.icon_alter), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(184.dp) + ) + } + + Spacer(modifier = Modifier.size(48.dp)) + Text( + text = stringResource(R.string.auth_title_1), + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.auth_title_2), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(48.dp)) + OutlinedTextField( modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), - value = inputText, + shape = RoundedCornerShape(8.dp), + value = login, onValueChange = { - inputText = it - viewModel.onIntent(AuthIntent.TextInput(it)) + login = it + viewModel.onIntent(AuthIntent.TextInput(login, password)) }, - label = { Text(stringResource(R.string.auth_label)) } + placeholder = { Text(stringResource(R.string.auth_placeholder_login)) }, + label = { Text(stringResource(R.string.auth_label_login)) } ) Spacer(modifier = Modifier.size(16.dp)) - Button( - modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), - onClick = { - viewModel.onIntent(AuthIntent.Send(inputText)) + OutlinedTextField( + modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + value = password, + onValueChange = { + password = it + viewModel.onIntent(AuthIntent.TextInput(login, password)) }, - enabled = state.isEnabledSend - ) { - Text(stringResource(R.string.auth_sign_in)) - } + placeholder = { Text(stringResource(R.string.auth_placeholder_password)) }, + label = { Text(stringResource(R.string.auth_label_passord)) } + ) + Spacer(modifier = Modifier.size(16.dp)) if (state.error != null) { + Card( + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.testTag(TestIds.Auth.ERROR).padding(16.dp), + text = state.error, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + + } + Spacer(modifier = Modifier.size(20.dp)) + Button( + modifier = Modifier + .testTag(TestIds.Auth.SIGN_BUTTON) + .fillMaxWidth() + .height(64.dp), + onClick = { + viewModel.onIntent(AuthIntent.Send(login, password)) + }, + enabled = state.isEnabledSend, + ) { Text( - modifier = Modifier.testTag(TestIds.Auth.ERROR), - text = state.error, - style = MaterialTheme.typography.bodyMedium, - color = Color.Red, + text = stringResource(R.string.auth_sign_in), + style = MaterialTheme.typography.titleMedium, ) } } \ 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 c28f5cd..4fa6790 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 @@ -1,5 +1,6 @@ package ru.myitschool.work.ui.screen.auth +import android.os.CountDownTimer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -9,14 +10,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow 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.Data( isEnabledSend = false, @@ -28,29 +30,68 @@ class AuthViewModel : ViewModel() { private val _actionFlow: MutableSharedFlow = MutableSharedFlow() val actionFlow: SharedFlow = _actionFlow + private var authTries: Int = 0 + private var timerSecs: Int = 0 + + private var login: String = "" + private var password: String = "" + fun onIntent(intent: AuthIntent) { when (intent) { is AuthIntent.Send -> { - viewModelScope.launch { - checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( - onSuccess = { - _actionFlow.emit(AuthAction.Open(MainScreenDestination)) - }, - onFailure = { error -> + if (authTries >= 5) { + timerSecs = Constants.AUTH_DELAY + val timer = object : CountDownTimer((timerSecs * 1000).toLong(), 1000) { + override fun onTick(millisUntilFinished: Long) { + timerSecs -= 1 updateStateIfData { oldState -> oldState.copy( - error = error.message + isEnabledSend = false, + error = "Слишком много попыток входа, попробуйте через $timerSecs секунд" ) } } - ) + + override fun onFinish() { + authTries = 0 + updateStateIfData { oldState -> + oldState.copy( + isEnabledSend = checkCodeFormatUseCase.invoke( + login = login, + password = password + ), + error = null + ) + } + } + } + timer.start() + } else { + viewModelScope.launch { + _uiState.update { AuthState.Loading } + checkAndSaveAuthCodeUseCase.invoke(intent.login, intent.password).fold( + onSuccess = { + _actionFlow.emit(AuthAction.Open(MainScreenDestination)) + }, + onFailure = { error -> + authTries += 1 + _uiState.update { AuthState.Data(isEnabledSend = false, error = error.message) } + } + ) + } } } + is AuthIntent.TextInput -> { + login = intent.login + password = intent.password updateStateIfData { oldState -> oldState.copy( - isEnabledSend = checkCodeFormatUseCase.invoke(intent.text), - error = null + isEnabledSend = checkCodeFormatUseCase.invoke( + login = intent.login, + password = intent.password + ) && authTries <= 5, + error = if (authTries >= 5) oldState.error else null ) } } 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 index 9269095..b084f65 100644 --- 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 @@ -4,6 +4,6 @@ sealed interface BookIntent { data object Refresh: BookIntent data class Add( val date: String, - val placeId: String + val placeId: Int ): BookIntent } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt index 60842f3..c707dbf 100644 --- a/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt +++ b/app/src/main/java/ru/myitschool/work/ui/screen/book/BookScreen.kt @@ -170,7 +170,7 @@ private fun ContentState( mutableIntStateOf(startDestination.index) } var selectedPlaceId by rememberSaveable { - mutableStateOf(null) + mutableStateOf(null) } Box { Column { @@ -213,7 +213,7 @@ private fun ContentState( startDestination = startDestination, state = state, onPlaceSelected = { id -> - selectedPlaceId = id + selectedPlaceId = id.toInt() } ) } diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/room/CardState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/room/CardState.kt new file mode 100644 index 0000000..18e095f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/room/CardState.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.room + +sealed interface CardState { + data object Booked: CardState + data object Open: CardState + data object BookedByMe: CardState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomIntent.kt b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomIntent.kt new file mode 100644 index 0000000..98f2fe0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomIntent.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.ui.screen.room + +sealed interface RoomIntent { + data class Refresh(val placeId: Int): RoomIntent + data class Booking(val placeId: Int, val date: String): RoomIntent + data class UnBook(val placeId: Int, val date: String): RoomIntent +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomScreen.kt b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomScreen.kt new file mode 100644 index 0000000..17d321a --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomScreen.kt @@ -0,0 +1,153 @@ +package ru.myitschool.work.ui.screen.room + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.myitschool.work.core.TestIds +import ru.myitschool.work.ui.screen.auth.AuthViewModel + +@Composable +fun RoomScreen( + viewModel: RoomViewModel = viewModel(), + roomId: Int, + navController: NavController +) { + val state by viewModel.uiState.collectAsState() + + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLow) + ) { + when(val currentState = state) { + is RoomState.Data -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(20.dp) + ) { + Text( + text = "TEST_LOCATOIN", + style = MaterialTheme.typography.displayMedium + ) + Spacer(modifier = Modifier.size(12.dp)) + LazyColumn { + itemsIndexed(currentState.data.entries.toList()) { index, item -> + DayCard(item.key, item.value.toString(), index) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + ) { + Button( + onClick = { viewModel.onIntent(RoomIntent.Booking(roomId, currentState.data.keys.toList()[0])) }, + shape = RoundedCornerShape(64, 0, 0, 64) + ) { + Text("Book") + } + Button( + onClick = { viewModel.onIntent(RoomIntent.UnBook(roomId, currentState.data.keys.toList()[0])) }, + shape = RoundedCornerShape(0, 64, 64, 0) + ) { + Text("Cancel") + } + } + + + } + is RoomState.Error -> { + Card( + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + ), + ) { + Text( + modifier = Modifier.testTag(TestIds.Auth.ERROR).padding(16.dp), + text = currentState.error, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + } + Button( + onClick = { viewModel.onIntent(RoomIntent.Refresh(roomId)) } + ) { + Text("Refresh") + } + } + is RoomState.Loading -> { + CircularProgressIndicator() + } + } + } +} + +@Composable +fun DayCard(name: String?, day: String, index: Int) { + Card( + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = if (name == null) { + MaterialTheme.colorScheme.surfaceContainerLowest + } else if (name == "hitler") { + MaterialTheme.colorScheme.secondaryContainer + } else { + MaterialTheme.colorScheme.tertiaryContainer + }, //REMAKE TO STATE + ), + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(24.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "${name}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "${day}", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + Text("${index}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomState.kt b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomState.kt new file mode 100644 index 0000000..746a1c5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomState.kt @@ -0,0 +1,15 @@ +package ru.myitschool.work.ui.screen.room + +import ru.myitschool.work.domain.room.entities.RoomEntity +import ru.myitschool.work.ui.screen.main.MainState + +sealed interface RoomState { + data object Loading: RoomState + data class Error( + val error: String + ): RoomState + data class Data( + val data: Map, + val name: String + ): RoomState +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomViewModel.kt new file mode 100644 index 0000000..201a626 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/screen/room/RoomViewModel.kt @@ -0,0 +1,124 @@ +package ru.myitschool.work.ui.screen.room + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +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 +import ru.myitschool.work.ui.nav.RoomScreenDestination + + +class RoomViewModel( + savedStateHandle: SavedStateHandle, +): ViewModel() { + + private val roomScreen = savedStateHandle.toRoute() + + 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.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + refresh(roomScreen.roomId) +// Timer().schedule(object : TimerTask() { +// override fun run() { +// Log.d("mytimer", "A Kiss every 5 seconds") +// } +// }, 0, 5000) + } + + fun onIntent(intent: RoomIntent) { + when (intent) { + is RoomIntent.Refresh -> { + refresh(intent.placeId) + } + + is RoomIntent.Booking -> { + viewModelScope.launch { + sendRoomBookingRequestUseCase.invoke( + intent.placeId, + intent.date + ).fold( + onSuccess = { + refresh(intent.placeId) + }, + onFailure = { error -> + RoomState.Error( + error = error.message.orEmpty() + ) + } + ) + } + } + + is RoomIntent.UnBook -> { + viewModelScope.launch { + deleteRoomBookingRequestUseCase.invoke( + intent.placeId, + intent.date + ).fold( + onSuccess = { + refresh(intent.placeId) + }, + onFailure = { error -> + RoomState.Error( + error = error.message.orEmpty() + ) + } + ) + } + } + } + } + + private fun refresh(roomId: Int) { + viewModelScope.launch { + _uiState.update { RoomState.Loading } + _uiState.update { + getRoomBookingsDataUseCase.invoke(roomId = roomId).fold( + onSuccess = { data -> + RoomState.Data( + data = data.data, + name = data.name + ) + }, + onFailure = { error -> + RoomState.Error( + error = error.message.orEmpty() + ) + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/difference.xml b/app/src/main/res/drawable/difference.xml new file mode 100644 index 0000000..213c6e8 --- /dev/null +++ b/app/src/main/res/drawable/difference.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a9273cf..82b7cfd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,12 @@ Work RootActivity - Привет! Введи код для авторизации - Код + Войдите + Используя аккаунт S-App + Логин + Введите логин + Пароль + Введите пароль Войти Обновить @@ -12,4 +16,6 @@ Забронировать Назад Всё забронировано + + Иконка \ No newline at end of file