diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1eb6645..5836b18 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,11 @@ plugins { kotlinAnnotationProcessor id("com.google.dagger.hilt.android").version("2.51.1") } +configurations.all { + resolutionStrategy { + force("org.hamcrest:hamcrest-junit:2.0.0.0") + } +} val packageName = "ru.myitschool.work" @@ -29,15 +34,37 @@ android { targetCompatibility = Version.Kotlin.javaSource } + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + kotlinOptions { jvmTarget = Version.Kotlin.jvmTarget } } dependencies { + + val fragmentVersion = "1.8.6" + debugImplementation("androidx.fragment:fragment-testing-manifest:$fragmentVersion") + androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion") + + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") + + testImplementation("androidx.test:core:1.6.1") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.14") + defaultLibrary() + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") + implementation("androidx.paging:paging-runtime:3.3.6") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") val ktorClientCore = "3.0.3" @@ -54,9 +81,6 @@ dependencies { implementation(Dependencies.AndroidX.Navigation.fragment) implementation(Dependencies.AndroidX.Navigation.navigationUi) - implementation(Dependencies.Retrofit.library) - implementation(Dependencies.Retrofit.gsonConverter) - implementation("com.squareup.picasso:picasso:2.8") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("androidx.datastore:datastore-preferences:1.1.2") diff --git a/app/src/androidTest/kotlin/LoginFragmentTest.kt b/app/src/androidTest/kotlin/LoginFragmentTest.kt new file mode 100644 index 0000000..2758ab7 --- /dev/null +++ b/app/src/androidTest/kotlin/LoginFragmentTest.kt @@ -0,0 +1,26 @@ +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.filters.LargeTest +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import ru.myitschool.work.R +import ru.myitschool.work.ui.login.LoginFragment +import utils.SwipeRefreshLayoutMatchers.isRefreshing + +@RunWith(AndroidJUnit4ClassRunner::class) +@LargeTest +class LoginFragmentTest { + @get:Rule + val fragmentRule = launchFragmentInContainer() + + @Test + fun onSwipeDataRefreshes() { + onView(withId(R.id.refresh)).perform(swipeDown()) + onView(withId(R.id.refresh)).check(matches(isRefreshing())) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/utils/SwipeRefreshLayoutMatchers.kt b/app/src/androidTest/kotlin/utils/SwipeRefreshLayoutMatchers.kt new file mode 100644 index 0000000..7a8cb5a --- /dev/null +++ b/app/src/androidTest/kotlin/utils/SwipeRefreshLayoutMatchers.kt @@ -0,0 +1,25 @@ +package utils + +import android.view.View +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.test.espresso.matcher.BoundedMatcher +import org.hamcrest.Description +import org.hamcrest.Matcher + +object SwipeRefreshLayoutMatchers { + + @JvmStatic + fun isRefreshing(): Matcher { + return object : BoundedMatcher( + SwipeRefreshLayout::class.java) { + + override fun describeTo(description: Description) { + description.appendText("is refreshing") + } + + override fun matchesSafely(view: SwipeRefreshLayout): Boolean { + return view.isRefreshing + } + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4172d4e..36286a8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + { + return networkDataSource.blockUser(login, localCredentialsLocalDataSource.getToken()) + } + + override suspend fun unblockUser(login: String): Result { + return networkDataSource.unblockUser(login, localCredentialsLocalDataSource.getToken()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/PassRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/PassRepositoryImpl.kt new file mode 100644 index 0000000..d93497f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/PassRepositoryImpl.kt @@ -0,0 +1,52 @@ +package ru.myitschool.work.data + +import android.util.Log +import ru.myitschool.work.data.dto.PassDto +import ru.myitschool.work.data.local.CredentialsLocalDataSource +import ru.myitschool.work.data.network.PassNetworkDataSource +import ru.myitschool.work.domain.entities.PassEntity +import ru.myitschool.work.domain.passes.PassRepository + +class PassRepositoryImpl( + private val networkDataSource: PassNetworkDataSource, + private val credentialsLocalDataSource: CredentialsLocalDataSource +) : PassRepository { + + override suspend fun getCurrentPasses(pageNum: Int, pageSize: Int): Result> { + return map( + networkDataSource.getCurrentPasses( + pageNum, + pageSize, + credentialsLocalDataSource.getToken() + ) + ) + } + + override suspend fun getUsersPasses( + pageNum: Int, + pageSize: Int, + login: String + ): Result> { + return map( + networkDataSource.getUsersPasses( + login = login, + pageNum = pageNum, + pageSize = pageSize, + token = credentialsLocalDataSource.getToken() + ) + ) + } + + private fun map(listDto: Result>): Result> { + return listDto.map { successListDto -> + successListDto.mapNotNull { dto -> + PassEntity( + type = dto.terminal?.type ?: return@mapNotNull null, + name = dto.terminal.name ?: return@mapNotNull null, + time = dto.time ?: return@mapNotNull null + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/QrRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/QrRepositoryImpl.kt index 69976ab..1e82e99 100644 --- a/app/src/main/java/ru/myitschool/work/data/QrRepositoryImpl.kt +++ b/app/src/main/java/ru/myitschool/work/data/QrRepositoryImpl.kt @@ -1,6 +1,7 @@ package ru.myitschool.work.data import ru.myitschool.work.data.local.CredentialsLocalDataSource +import ru.myitschool.work.data.network.QrNetworkDataSource import ru.myitschool.work.domain.entities.QrEntity import ru.myitschool.work.domain.qr.QrRepository diff --git a/app/src/main/java/ru/myitschool/work/data/UserRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/UserRepositoryImpl.kt index 8c86cd6..f2e8648 100644 --- a/app/src/main/java/ru/myitschool/work/data/UserRepositoryImpl.kt +++ b/app/src/main/java/ru/myitschool/work/data/UserRepositoryImpl.kt @@ -1,6 +1,5 @@ package ru.myitschool.work.data -import android.util.Log import ru.myitschool.work.data.dto.UserDto import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.UserLocalDataSource @@ -19,16 +18,29 @@ class UserRepositoryImpl( return runCatching { networkDataSource.login(credentialsLocalDataSource.updateToken(login, password)) - .onSuccess { dto -> - map(dto).onSuccess { userLocalDataSource.cacheData(it) } - } + .fold( + onSuccess = { dto -> + map(dto).fold( + onSuccess = { + userLocalDataSource.cacheData(it) + }, + onFailure = { error(it) } + ) + }, + onFailure = { error(it) } + ) } } override suspend fun authorize(token: String): Result { return networkDataSource.login(token).fold( - onSuccess = { Result.success(Unit) }, - onFailure = { error -> Result.failure(error) } + onSuccess = { dto -> + map(dto).fold( + onSuccess = { Result.success(userLocalDataSource.cacheData(it)) }, + onFailure = { Result.failure(it) } + ) + }, + onFailure = { Result.failure(it) } ) } @@ -45,15 +57,27 @@ class UserRepositoryImpl( return userLocalDataSource.getUser()!! } - private fun map(userDto: UserDto): Result { + private suspend fun map(userDto: UserDto): Result { return runCatching { UserEntity( - id = userDto.id ?: error("Null user id"), name = userDto.name ?: error("Null user name"), - lastVisit = userDto.lastVisit ?: error("Null user lastVisit"), + lastVisit = userDto.lastVisit ?: "", photoUrl = userDto.photoUrl ?: error("Null user photoUrl"), - position = userDto.position ?: error("Null user position") + position = userDto.position ?: error("Null user position"), + isAdmin = userDto.roleId?.let { it -> + networkDataSource.isRoleHasAdminPermissions( + it, + credentialsLocalDataSource.getToken() + ).fold(onSuccess = { it }, onFailure = { error(it) }) + } ?: error("Null user roleId"), + isCardBlocked = userDto.isCardBlocked ?: error("Null user isCardBlocked") ) } } + + override suspend fun getUserByLogin(login: String): Result { + return networkDataSource.getUserByLogin(login, credentialsLocalDataSource.getToken()).fold( + onSuccess = { map(it) }, onFailure = { Result.failure(it) } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/dto/PassDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/PassDto.kt new file mode 100644 index 0000000..bba3323 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/PassDto.kt @@ -0,0 +1,21 @@ +package ru.myitschool.work.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class PassDto( + @SerialName("localDateTime") + val time: String?, + @SerialName("terminal") + val terminal: TerminalDto? +) + +@Serializable +data class TerminalDto( + @SerialName("type") + val type: String?, + @SerialName("name") + val name: String? +) diff --git a/app/src/main/java/ru/myitschool/work/data/dto/UserDto.java b/app/src/main/java/ru/myitschool/work/data/dto/UserDto.java deleted file mode 100644 index 0e5f2cf..0000000 --- a/app/src/main/java/ru/myitschool/work/data/dto/UserDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.myitschool.work.data.dto; - - -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import kotlinx.serialization.Serializable; - -@Serializable -public class UserDto { - - @Nullable - @SerializedName("id") - public String id; - @Nullable - @SerializedName("name") - public String name; - @Nullable - @SerializedName("lastVisit") - public String lastVisit; - @Nullable - @SerializedName("photo") - public String photoUrl; - @Nullable - @SerializedName("position") - public String position; - @Nullable - @SerializedName("login") - public String login; -} diff --git a/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt b/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt new file mode 100644 index 0000000..c4af54e --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt @@ -0,0 +1,22 @@ +package ru.myitschool.work.data.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserDto( + @SerialName("authority_id") + val roleId: Int?, + @SerialName("name") + val name: String?, + @SerialName("lastVisit") + val lastVisit: String?, + @SerialName("photo") + val photoUrl: String?, + @SerialName("position") + val position: String?, + @SerialName("username") + val login: String?, + @SerialName("isCardBlocked") + val isCardBlocked: Boolean? +) diff --git a/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt new file mode 100644 index 0000000..379e376 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt @@ -0,0 +1,44 @@ +package ru.myitschool.work.data.network + +import io.ktor.client.request.headers +import io.ktor.client.request.patch +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.myitschool.work.core.Constants.SERVER_ADDRESS + +object AdminNetworkDataSource { + + private val client = KtorClient.client + + suspend fun blockUser(login: String, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = client.patch("$SERVER_ADDRESS/api/users/block?username=$login") { + headers { + append(HttpHeaders.Authorization, token) + } + } + + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + Unit + } + } + + suspend fun unblockUser(login: String, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = client.patch("$SERVER_ADDRESS/api/users/unblock?username=$login") { + headers { + append(HttpHeaders.Authorization, token) + } + } + + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + Unit + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/data/network/PassNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/network/PassNetworkDataSource.kt new file mode 100644 index 0000000..de063aa --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/network/PassNetworkDataSource.kt @@ -0,0 +1,56 @@ +package ru.myitschool.work.data.network + +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.myitschool.work.core.Constants.SERVER_ADDRESS +import ru.myitschool.work.data.dto.PassDto + +object PassNetworkDataSource { + private val client = KtorClient.client + + suspend fun getCurrentPasses( + pageNum: Int, + pageSize: Int, + token: String + ): Result> = + withContext(Dispatchers.IO) { + runCatching { + val response = + client.get("$SERVER_ADDRESS/api/passes/paginated/?page=$pageNum&size=$pageSize") { + headers { + append(HttpHeaders.Authorization, token) + } + } + if (response.status != HttpStatusCode.OK) + error("${response.status}") + response.body() + } + + } + + suspend fun getUsersPasses( + login: String, + pageNum: Int, + pageSize: Int, + token: String + ): Result> = withContext(Dispatchers.IO) { + runCatching { + + val response = + client.get("$SERVER_ADDRESS/api/passes?login=$login&pageNum=$pageNum&pageSize=$pageSize") { + headers { + append(HttpHeaders.Authorization, token) + } + } + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + response.body() + } + + } +} diff --git a/app/src/main/java/ru/myitschool/work/data/network/QrNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/network/QrNetworkDataSource.kt index 2ad9c06..478d93e 100644 --- a/app/src/main/java/ru/myitschool/work/data/network/QrNetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/network/QrNetworkDataSource.kt @@ -1,4 +1,4 @@ -package ru.myitschool.work.data +package ru.myitschool.work.data.network import io.ktor.client.request.headers import io.ktor.client.request.patch @@ -11,28 +11,25 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ru.myitschool.work.core.Constants import ru.myitschool.work.data.dto.QrDto -import ru.myitschool.work.data.network.KtorClient import ru.myitschool.work.domain.entities.QrEntity object QrNetworkDataSource { - suspend fun pushQr(qrEntity: QrEntity, token: String): Result = withContext(Dispatchers.IO) { - - runCatching { - - val response = KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/push_qr") { - headers { - append(HttpHeaders.Authorization, token) - } - contentType(ContentType.Application.Json) - setBody( - QrDto(code = qrEntity.code) - ) + suspend fun pushQr(qrEntity: QrEntity, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = + KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/open") { + headers { + append(HttpHeaders.Authorization, token) + } + contentType(ContentType.Application.Json) + setBody(QrDto(qrEntity.code)) + } + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + Unit } - if (response.status != HttpStatusCode.OK) - error("Status ${response.status}") - Unit } - } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/network/UserNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/network/UserNetworkDataSource.kt index 6fb986b..915f6e4 100644 --- a/app/src/main/java/ru/myitschool/work/data/network/UserNetworkDataSource.kt +++ b/app/src/main/java/ru/myitschool/work/data/network/UserNetworkDataSource.kt @@ -22,16 +22,45 @@ object UserNetworkDataSource { suspend fun login(token: String): Result = withContext(Dispatchers.IO) { runCatching { - val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") { - headers { - append(HttpHeaders.Authorization, token) + val result = + KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") { + headers { + append(HttpHeaders.Authorization, token) + } } - } if (result.status != HttpStatusCode.OK) error("Status ${result.status}") result.body() } } - suspend fun getUserByLogin() {} + suspend fun isRoleHasAdminPermissions(roleId: Int, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = + KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/authority/$roleId") { + headers { + append(HttpHeaders.Authorization, token) + } + } + + response.status == HttpStatusCode.OK + } + } + + suspend fun getUserByLogin(login: String, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = + KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/get?username=${login}") { + headers { + append(HttpHeaders.Authorization, token) + } + } + + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + response.body() + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt b/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt new file mode 100644 index 0000000..acfe197 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.domain.admin + +interface AdminRepository { + suspend fun blockUser(login: String): Result + suspend fun unblockUser(login: String): Result +} diff --git a/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt new file mode 100644 index 0000000..1a90483 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.domain.admin + +class BlockUserUseCase( + private val repository: AdminRepository +) { + + suspend operator fun invoke(login: String) = repository.blockUser(login) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/admin/UnblockUserUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/admin/UnblockUserUseCase.kt new file mode 100644 index 0000000..fe28dca --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/admin/UnblockUserUseCase.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.domain.admin + +class UnblockUserUseCase( + private val repository: AdminRepository +) { + + suspend operator fun invoke(login: String) = repository.unblockUser(login) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/entities/PassEntity.kt b/app/src/main/java/ru/myitschool/work/domain/entities/PassEntity.kt new file mode 100644 index 0000000..52319a7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/entities/PassEntity.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.domain.entities + +data class PassEntity( + val type: String, + val name: String, + val time: String +) 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 index 2e9d7b7..483d9ac 100644 --- a/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt +++ b/app/src/main/java/ru/myitschool/work/domain/entities/UserEntity.kt @@ -1,9 +1,10 @@ package ru.myitschool.work.domain.entities data class UserEntity( - val id: String, + val isAdmin: Boolean, val name: String, val lastVisit: String, val photoUrl: String, val position: String, + val isCardBlocked: Boolean ) diff --git a/app/src/main/java/ru/myitschool/work/domain/passes/GetCurrentPassesUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/passes/GetCurrentPassesUseCase.kt new file mode 100644 index 0000000..33f7206 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/passes/GetCurrentPassesUseCase.kt @@ -0,0 +1,11 @@ +package ru.myitschool.work.domain.passes + +import ru.myitschool.work.domain.entities.PassEntity + +class GetCurrentPassesUseCase( + private val repository: PassRepository +) { + + suspend operator fun invoke(pageNum: Int, pageSize: Int): Result> = + repository.getCurrentPasses(pageNum, pageSize) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt new file mode 100644 index 0000000..c41854f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.passes + +class GetUsersPassesUseCase( + private val repository: PassRepository +) { + + suspend operator fun invoke(pageNum: Int, pageSize: Int, login: String) = + repository.getUsersPasses(pageNum, pageSize, login) +} diff --git a/app/src/main/java/ru/myitschool/work/domain/passes/PassRepository.kt b/app/src/main/java/ru/myitschool/work/domain/passes/PassRepository.kt new file mode 100644 index 0000000..580c46f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/passes/PassRepository.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.passes + +import ru.myitschool.work.domain.entities.PassEntity + +interface PassRepository { + + suspend fun getCurrentPasses(pageNum: Int, pageSize: Int): Result> + suspend fun getUsersPasses(pageNum: Int, pageSize: Int, login: String): Result> +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/user/GetUserByLoginUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/user/GetUserByLoginUseCase.kt new file mode 100644 index 0000000..6e6d02b --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/user/GetUserByLoginUseCase.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.domain.user + +import ru.myitschool.work.domain.entities.UserEntity + +class GetUserByLoginUseCase( + private val repository: UserRepository +) { + + suspend operator fun invoke(login: String): Result = repository.getUserByLogin(login) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/user/UserRepository.kt b/app/src/main/java/ru/myitschool/work/domain/user/UserRepository.kt index 3ea221d..85f356e 100644 --- a/app/src/main/java/ru/myitschool/work/domain/user/UserRepository.kt +++ b/app/src/main/java/ru/myitschool/work/domain/user/UserRepository.kt @@ -5,4 +5,5 @@ import ru.myitschool.work.domain.entities.UserEntity interface UserRepository { suspend fun getCurrentUser(): UserEntity + suspend fun getUserByLogin(login: String) : Result } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/NoInternetNotificationFragment.kt b/app/src/main/java/ru/myitschool/work/ui/NoInternetNotificationFragment.kt new file mode 100644 index 0000000..eaa1f68 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/NoInternetNotificationFragment.kt @@ -0,0 +1,27 @@ +package ru.myitschool.work.ui + +import android.os.Bundle +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentNoInternetNotificationBinding +import ru.myitschool.work.utils.isOnline + +class NoInternetNotificationFragment: BottomSheetDialogFragment(R.layout.fragment_no_internet_notification) { + + private var _binding: FragmentNoInternetNotificationBinding? = null + private val binding: FragmentNoInternetNotificationBinding get() = _binding!! + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentNoInternetNotificationBinding.bind(view) + + binding.close.setOnClickListener { + if (isOnline(requireActivity())) dismiss() + } + } + + override fun onDestroy() { + _binding = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt index 3ff6bc2..653854d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -4,13 +4,18 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint import ru.myitschool.work.R +import ru.myitschool.work.utils.isOnline -// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! @AndroidEntryPoint class RootActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_root) + if (!isOnline(this)) { + val dialog = NoInternetNotificationFragment() + dialog.isCancelable = false + dialog.show(supportFragmentManager, "NO_INTERNET") + } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt new file mode 100644 index 0000000..20d22b4 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt @@ -0,0 +1,41 @@ +package ru.myitschool.work.ui.admin.search + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentFindEmployeeBinding +import ru.myitschool.work.utils.collectWithLifecycle + +class AdminFragment : Fragment(R.layout.fragment_find_employee) { + + private var _binding: FragmentFindEmployeeBinding? = null + private val binding: FragmentFindEmployeeBinding get() = _binding!! + + private val viewModel by viewModels { AdminViewModel.Factory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentFindEmployeeBinding.bind(view) + + viewModel.state.collectWithLifecycle(this) { state -> + binding.username.isEnabled = state !is AdminViewModel.State.Loading + when (state) { + is AdminViewModel.State.Error -> binding.error.text = state.errorMessage + is AdminViewModel.State.Loading -> Unit + is AdminViewModel.State.Show -> findNavController().navigate(R.id.action_adminFragment_to_viewUserAsAdminFragment) + is AdminViewModel.State.Waiting -> Unit + } + } + + binding.find.setOnClickListener { + viewModel.onFind(binding.username.text.toString()) + } + } + + override fun onDestroy() { + _binding = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt new file mode 100644 index 0000000..4d80b96 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt @@ -0,0 +1,139 @@ +package ru.myitschool.work.ui.admin.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ru.myitschool.work.data.AdminRepositoryImpl +import ru.myitschool.work.data.PassRepositoryImpl +import ru.myitschool.work.data.UserRepositoryImpl +import ru.myitschool.work.data.local.CredentialsLocalDataSource +import ru.myitschool.work.data.local.UserLocalDataSource +import ru.myitschool.work.data.network.AdminNetworkDataSource +import ru.myitschool.work.data.network.PassNetworkDataSource +import ru.myitschool.work.data.network.UserNetworkDataSource +import ru.myitschool.work.domain.admin.BlockUserUseCase +import ru.myitschool.work.domain.admin.UnblockUserUseCase +import ru.myitschool.work.domain.entities.PassEntity +import ru.myitschool.work.domain.entities.UserEntity +import ru.myitschool.work.domain.passes.GetUsersPassesUseCase +import ru.myitschool.work.domain.user.GetUserByLoginUseCase +import ru.myitschool.work.ui.admin.view.UsersPassesPagingSource + +class AdminViewModel( + private val getUserByLoginUseCase: GetUserByLoginUseCase, + private val getUsersPassesUseCase: GetUsersPassesUseCase, + private val unBlockUserUseCase: UnblockUserUseCase, + private val blockUserUseCase: BlockUserUseCase +) : ViewModel() { + + private val _state = MutableStateFlow(State.Waiting) + val state = _state.asStateFlow() + +// private var _listState: Flow>? = null +// val listState: Flow> get() = _listState!! + + private var currentLogin: String = "pivanov" + + fun onFind(login: String) { + viewModelScope.launch { + _state.emit(State.Loading) + getUserByLoginUseCase(login).fold( + onSuccess = { data -> + currentLogin = login + _state.emit(State.Show(data)) + setUpPager(login) + }, + onFailure = { _state.emit(State.Error(it.message.toString())) } + ) + } + } + + fun onRefresh() { + updateState() + } + + private fun updateState() { + viewModelScope.launch { + _state.emit(State.Loading) + getUserByLoginUseCase(currentLogin!!).fold( + onSuccess = { _state.emit(State.Show(it)) }, + onFailure = { _state.emit(State.Error(it.message.toString())) } + ) + } + } + + private fun setUpPager(login: String) { +// _listState = Pager( +// config = PagingConfig( +// pageSize = 10, +// enablePlaceholders = false, +// maxSize = 50 +// ) +// ) { +// UsersPassesPagingSource(getUsersPassesUseCase::invoke, login) +// }.flow +// .cachedIn(viewModelScope) + } + + fun onBlock() { + viewModelScope.launch { + blockUserUseCase(currentLogin!!) + } + } + + fun unblock() { + viewModelScope.launch { + unBlockUserUseCase(currentLogin!!) + } + } + + sealed interface State { + data class Show(val user: UserEntity) : State + data object Waiting : State + data object Loading : State + data class Error(val errorMessage: String) : State + } + + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class, extras: CreationExtras): T { + val adminRepository = AdminRepositoryImpl( + networkDataSource = AdminNetworkDataSource, + localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance() + ) + return AdminViewModel( + getUserByLoginUseCase = GetUserByLoginUseCase( + repository = UserRepositoryImpl( + credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(), + userLocalDataSource = UserLocalDataSource, + networkDataSource = UserNetworkDataSource + ) + ), + getUsersPassesUseCase = GetUsersPassesUseCase( + repository = PassRepositoryImpl( + networkDataSource = PassNetworkDataSource, + credentialsLocalDataSource = CredentialsLocalDataSource.getInstance() + ) + ), + blockUserUseCase = BlockUserUseCase( + repository = adminRepository + ), + unBlockUserUseCase = UnblockUserUseCase( + repository = adminRepository + ) + ) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt b/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt new file mode 100644 index 0000000..34789b9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.ui.admin.view + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import ru.myitschool.work.domain.entities.PassEntity + +class UsersPassesPagingSource( + private val request: suspend (pageNum: Int, pageSize: Int, login: String) -> Result>, + private val login: String +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) + ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val pageNum = params.key ?: 0 + return request.invoke(pageNum, params.loadSize, login).fold( + onSuccess = { value -> + LoadResult.Page( + data = value, + prevKey = (pageNum - 1).takeIf { it >= 0 }, + nextKey = (pageNum + 1).takeIf { value.size == params.loadSize } + ) + }, + onFailure = { error -> LoadResult.Error(error) } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt b/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt new file mode 100644 index 0000000..e7e5b40 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt @@ -0,0 +1,90 @@ +package ru.myitschool.work.ui.admin.view + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.paging.LoadState +import com.squareup.picasso.Picasso +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentUserBinding +import ru.myitschool.work.ui.admin.search.AdminViewModel +import ru.myitschool.work.ui.profile.PassesListAdapter +import ru.myitschool.work.utils.collectWithLifecycle +import ru.myitschool.work.utils.visibleOrGone + +class ViewUserAsAdminFragment : Fragment(R.layout.fragment_user) { + + private var _binding: FragmentUserBinding? = null + private val binding: FragmentUserBinding get() = _binding!! + + private val viewModel by viewModels { AdminViewModel.Factory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentUserBinding.bind(view) + + binding.findUser.visibleOrGone(false) + binding.logout.visibleOrGone(false) + + viewModel.state.collectWithLifecycle(this) { state -> + binding.refresh.isRefreshing = state is AdminViewModel.State.Loading + binding.content.visibleOrGone(state is AdminViewModel.State.Show) + + when (state) { + is AdminViewModel.State.Loading -> Unit + is AdminViewModel.State.Show -> { + val user = state.user + binding.block.visibleOrGone(!user.isCardBlocked) + binding.unblock.visibleOrGone(user.isCardBlocked) + binding.fullname.text = user.name + binding.position.text = user.position + binding.lastEntry.text = user.lastVisit + Picasso.get().load(user.photoUrl).into(binding.photo) + } + + is AdminViewModel.State.Error -> binding.error.text = state.errorMessage + is AdminViewModel.State.Waiting -> Unit + } + } + +// viewModel.listState.collectWithLifecycle(this) { listState -> +// adapter.submitData(listState) +// } + +// adapter.loadStateFlow.collectWithLifecycle(this) { data -> +// val dataState = data.refresh +// binding.refresh.isRefreshing = dataState is LoadState.Loading +// binding.error.visibleOrGone(dataState is LoadState.Error) +// +// if (dataState is LoadState.Error) { +// binding.error.text = dataState.error.toString() +// } +// } + + binding.refresh.setOnRefreshListener { + viewModel.onRefresh() +// adapter.refresh() + } + + + binding.block.setOnClickListener { + AlertDialog.Builder(requireContext()) + .setTitle("Блокировка доступа") + .setMessage("Вы уверены, что хотите заблокировать доступ работнику: ${binding.fullname}?") + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.onBlock() } + .show() + } + + binding.unblock.setOnClickListener { + viewModel.unblock() + } + + } + + override fun onDestroy() { + _binding = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt index 58d6cd7..8b20cfc 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -1,8 +1,9 @@ package ru.myitschool.work.ui.login +import android.graphics.Color import android.os.Bundle -import android.util.Log import android.view.View +import androidx.core.content.ContextCompat import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -32,11 +33,18 @@ class LoginFragment : Fragment(R.layout.fragment_login) { binding.error.visibleOrGone(state is LoginViewModel.State.Error) when (state) { - is LoginViewModel.State.Error -> binding.error.text = state.errorMessage - is LoginViewModel.State.Loading -> Unit + is LoginViewModel.State.Error -> { + binding.error.text = state.errorMessage + setButtonActive() + } + is LoginViewModel.State.Loading -> setButtonInactive() is LoginViewModel.State.Waiting -> Unit - is LoginViewModel.State.LoginCheckCompleted -> binding.login.isEnabled = - state.isCompleted + is LoginViewModel.State.LoginCheckCompleted -> { + state.isCompleted.let { + if (it) setButtonActive() else setButtonInactive() + binding.login.isEnabled = it + } + } } } @@ -57,6 +65,18 @@ class LoginFragment : Fragment(R.layout.fragment_login) { } } + private fun setButtonInactive() { + binding.login.setTextColor(Color.BLACK) + binding.login.background = + ContextCompat.getDrawable(requireContext(), R.drawable.inactive_button) + } + + private fun setButtonActive() { + binding.login.setTextColor(Color.WHITE) + binding.login.background = + ContextCompat.getDrawable(requireContext(), R.drawable.main_button) + } + override fun onDestroy() { _binding = null super.onDestroy() diff --git a/app/src/main/java/ru/myitschool/work/ui/login/SplashFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/SplashFragment.kt index 500d5da..25435f1 100644 --- a/app/src/main/java/ru/myitschool/work/ui/login/SplashFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/login/SplashFragment.kt @@ -9,14 +9,10 @@ import androidx.navigation.fragment.findNavController import ru.myitschool.work.R import ru.myitschool.work.core.Constants import ru.myitschool.work.data.local.CredentialsLocalDataSource -import ru.myitschool.work.databinding.FragmentSplashBinding import ru.myitschool.work.utils.collectWithLifecycle class SplashFragment : Fragment(R.layout.fragment_splash) { - private var _binding: FragmentSplashBinding? = null - private val binding: FragmentSplashBinding get() = _binding!! - private val viewModel by viewModels { LoginViewModel.Factory } override fun onCreate(savedInstanceState: Bundle?) { @@ -30,21 +26,15 @@ class SplashFragment : Fragment(R.layout.fragment_splash) { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - _binding = FragmentSplashBinding.bind(view) viewModel.action.collectWithLifecycle(this) { action -> val navController = findNavController() when (action) { is LoginViewModel.Action.GoToLogin -> navController.navigate(R.id.loginFragment) is LoginViewModel.Action.OpenApp -> - navController.navigate(R.id.action_loginFragment_to_userFragment) + navController.navigate(R.id.action_splashFragment_to_userFragment) } } } - - override fun onDestroy() { - _binding = null - super.onDestroy() - } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/PassesListAdapter.kt b/app/src/main/java/ru/myitschool/work/ui/profile/PassesListAdapter.kt new file mode 100644 index 0000000..8c25790 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/profile/PassesListAdapter.kt @@ -0,0 +1,45 @@ +package ru.myitschool.work.ui.profile + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import ru.myitschool.work.databinding.PassItemBinding +import ru.myitschool.work.domain.entities.PassEntity + +class PassesListAdapter: + ListAdapter(CenterDiff) { + + class ViewHolder( + private val binding: PassItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: PassEntity) { + binding.time.text = item.time + binding.name.text = item.name + binding.type.text = item.type + } + } + + object CenterDiff : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: PassEntity, newItem: PassEntity): Boolean { + return oldItem.name == newItem.name + } + + override fun areItemsTheSame(oldItem: PassEntity, newItem: PassEntity): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + PassItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/PassesPagingSource.kt b/app/src/main/java/ru/myitschool/work/ui/profile/PassesPagingSource.kt new file mode 100644 index 0000000..1ee7815 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/profile/PassesPagingSource.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.ui.profile + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import ru.myitschool.work.domain.entities.PassEntity + +class PassesPagingSource( + private val request: suspend (pageNum: Int, pageSize: Int) -> Result> +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) + ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val pageNum = params.key ?: 0 + return request.invoke(pageNum, params.loadSize).fold( + onSuccess = { value -> + LoadResult.Page( + data = value, + prevKey = (pageNum - 1).takeIf { it > 0 }, + nextKey = (pageNum + 1).takeIf { value.size == params.loadSize } + ) + }, + onFailure = { error -> LoadResult.Error(error) } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/UserFragment.kt b/app/src/main/java/ru/myitschool/work/ui/profile/UserFragment.kt index 0add493..3627e46 100644 --- a/app/src/main/java/ru/myitschool/work/ui/profile/UserFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/profile/UserFragment.kt @@ -1,17 +1,21 @@ package ru.myitschool.work.ui.profile import android.os.Bundle +import android.util.Log import android.view.View +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.squareup.picasso.Picasso import ru.myitschool.work.R import ru.myitschool.work.databinding.FragmentUserBinding +import ru.myitschool.work.ui.qr.result.RESPONSE_KEY +import ru.myitschool.work.ui.qr.scan.QrScanDestination import ru.myitschool.work.utils.collectWithLifecycle import ru.myitschool.work.utils.visibleOrGone + class UserFragment : Fragment(R.layout.fragment_user) { private var _binding: FragmentUserBinding? = null @@ -22,30 +26,59 @@ class UserFragment : Fragment(R.layout.fragment_user) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { _binding = FragmentUserBinding.bind(view) + val adapter = PassesListAdapter() + binding.passes.adapter = adapter + viewModel.state.collectWithLifecycle(this) { state -> - (binding.refresh as SwipeRefreshLayout).isRefreshing = - state is UserViewModel.State.Loading - binding.content?.visibleOrGone(state is UserViewModel.State.Show) + binding.refresh.isRefreshing = state is UserViewModel.State.Loading + binding.content.visibleOrGone(state is UserViewModel.State.Show) when (state) { is UserViewModel.State.Loading -> Unit is UserViewModel.State.Show -> { val user = state.userEntity + adapter.submitList(state.passes) + binding.scan.visibleOrGone(!user.isCardBlocked) + binding.findUser.visibleOrGone(user.isAdmin) binding.fullname.text = user.name binding.position.text = user.position binding.lastEntry.text = user.lastVisit Picasso.get().load(user.photoUrl).into(binding.photo) + } } - - (binding.refresh as SwipeRefreshLayout).setOnRefreshListener { - viewModel.onRefresh() - } - - binding.logout.setOnClickListener { - viewModel.onLogout() - findNavController().navigate(R.id.action_userFragment_to_loginFragment) - } } + + binding.refresh.setOnRefreshListener { + viewModel.onRefresh() + } + + binding.logout.setOnClickListener { + AlertDialog.Builder(requireContext()) + .setTitle("Выход") + .setMessage("Вы уверены, что хотите выйти из аккаунта?") + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.onLogout() + findNavController().navigate(R.id.action_userFragment_to_loginFragment) + } + .show() + } + + binding.findUser.setOnClickListener { + findNavController().navigate(R.id.action_userFragment_to_adminFragment) + } + + binding.scan.setOnClickListener { + findNavController().navigate(R.id.action_userFragment_to_qrScanFragment) + } + + parentFragmentManager.setFragmentResultListener( + QrScanDestination.REQUEST_KEY, this + ) { _, result -> + parentFragmentManager.setFragmentResult(RESPONSE_KEY, result) + findNavController().navigate(R.id.action_userFragment_to_qrResultFragment) + } + } override fun onDestroy() { diff --git a/app/src/main/java/ru/myitschool/work/ui/profile/UserViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/profile/UserViewModel.kt index d037094..c066eea 100644 --- a/app/src/main/java/ru/myitschool/work/ui/profile/UserViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/profile/UserViewModel.kt @@ -1,35 +1,63 @@ package ru.myitschool.work.ui.profile +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import ru.myitschool.work.data.PassRepositoryImpl import ru.myitschool.work.data.UserRepositoryImpl import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.UserLocalDataSource +import ru.myitschool.work.data.network.PassNetworkDataSource import ru.myitschool.work.data.network.UserNetworkDataSource +import ru.myitschool.work.domain.entities.PassEntity import ru.myitschool.work.domain.entities.UserEntity import ru.myitschool.work.domain.login.LogoutUseCase +import ru.myitschool.work.domain.passes.GetCurrentPassesUseCase import ru.myitschool.work.domain.user.GetCurrentUserUseCase class UserViewModel( private val getCurrentUserUseCase: GetCurrentUserUseCase, - private val logoutUseCase: LogoutUseCase + private val logoutUseCase: LogoutUseCase, + private val getCurrentPassesUseCase: GetCurrentPassesUseCase ) : ViewModel() { private val _state = MutableStateFlow(State.Loading) val state = _state.asStateFlow() + val listState = Pager( + config = PagingConfig( + pageSize = 10, + enablePlaceholders = false, + maxSize = 50 + ) + ) { + PassesPagingSource(getCurrentPassesUseCase::invoke) + }.flow + .cachedIn(viewModelScope) + init { updateState() } private fun updateState() { viewModelScope.launch { - State.Show(getCurrentUserUseCase()) + _state.emit(State.Loading) + + _state.emit( + State.Show( + getCurrentUserUseCase(), + getCurrentPassesUseCase(0, 30).getOrNull()!! + ) + ) + } } @@ -43,7 +71,7 @@ class UserViewModel( sealed interface State { data object Loading : State - data class Show(val userEntity: UserEntity) : State + data class Show(val userEntity: UserEntity, val passes: List) : State } companion object { @@ -57,7 +85,13 @@ class UserViewModel( ) return UserViewModel( getCurrentUserUseCase = GetCurrentUserUseCase(repository = repository), - logoutUseCase = LogoutUseCase(repository = repository) + logoutUseCase = LogoutUseCase(repository = repository), + getCurrentPassesUseCase = GetCurrentPassesUseCase( + repository = PassRepositoryImpl( + networkDataSource = PassNetworkDataSource, + credentialsLocalDataSource = CredentialsLocalDataSource.getInstance() + ) + ) ) as T } } diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt index a56876d..cc6ac00 100644 --- a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt +++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultFragment.kt @@ -8,7 +8,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import ru.myitschool.work.R import ru.myitschool.work.databinding.FragmentQrResultBinding -import ru.myitschool.work.ui.qr.scan.QrScanDestination +import ru.myitschool.work.domain.entities.QrEntity import ru.myitschool.work.ui.qr.scan.QrScanDestination.getDataIfExist import ru.myitschool.work.utils.collectWithLifecycle @@ -20,25 +20,18 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) { private var _binding: FragmentQrResultBinding? = null private val binding: FragmentQrResultBinding get() = _binding!! - private var _resultQr: String? = null - private val resultQr: String = _resultQr!! - private val viewModel by viewModels { QrResultViewModel.Factory } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { _binding = FragmentQrResultBinding.bind(view) - if (savedInstanceState != null) { - _resultQr = savedInstanceState.getString(QrScanDestination.REQUEST_KEY) - } - parentFragmentManager.setFragmentResultListener(RESPONSE_KEY, this) { _, result -> - _resultQr = getDataIfExist(result) - viewModel.update(resultQr) + getDataIfExist(result)?.let { viewModel.setQr(QrEntity(it)) } + viewModel.update() } viewModel.state.collectWithLifecycle(this) { state -> - if (_resultQr == null) { + if (viewModel._qrEntity == null) { binding.result.setText(R.string.door_closed) binding.close.background = ContextCompat.getDrawable(requireContext(), R.drawable.warn_button) @@ -65,12 +58,6 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) { } } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(QrScanDestination.REQUEST_KEY, resultQr) - } - override fun onDestroy() { _binding = null super.onDestroy() diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt index e69739c..d2d84bf 100644 --- a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt +++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResultViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.CreationExtras import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import ru.myitschool.work.data.QrNetworkDataSource +import ru.myitschool.work.data.network.QrNetworkDataSource import ru.myitschool.work.data.QrRepositoryImpl import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.domain.entities.QrEntity @@ -20,9 +20,16 @@ class QrResultViewModel( private val _state = MutableStateFlow(State.Loading) val state = _state.asStateFlow() - fun update(qrValue: String) { + var _qrEntity: QrEntity? = null + private val qrEntity: QrEntity get() = _qrEntity!! + + fun setQr(qrEntity: QrEntity) { + _qrEntity = qrEntity + } + + fun update() { viewModelScope.launch { - pushQrUseCase(QrEntity(code = qrValue)).fold( + pushQrUseCase(qrEntity).fold( onSuccess = { _state.emit(State.Show) }, onFailure = { _state.emit(State.Error(it.message.toString())) } ) diff --git a/app/src/main/java/ru/myitschool/work/utils/AccessibilityExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/AccessibilityExtensions.kt new file mode 100644 index 0000000..7783436 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/AccessibilityExtensions.kt @@ -0,0 +1,22 @@ +package ru.myitschool.work.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +fun isOnline(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (capabilities != null) { + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return true + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + return true + } + } + return false +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt index 3faef06..d4b0905 100644 --- a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt +++ b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt @@ -1,18 +1,19 @@ package ru.myitschool.work.utils import androidx.fragment.app.Fragment -import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -inline fun Flow.collectWithLifecycle( +fun Flow.collectWithLifecycle( fragment: Fragment, - crossinline collector: (T) -> Unit + function: suspend (T) -> Unit ) { fragment.viewLifecycleOwner.lifecycleScope.launch { - flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> - collector(value) + fragment.repeatOnLifecycle(Lifecycle.State.STARTED) { + collect { function.invoke(it) } } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index a4f78de..ca3826a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index cc14f03..4aebdda 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,26 @@ - - - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/drawable/no_wifi_pic.png b/app/src/main/res/drawable/no_wifi_pic.png new file mode 100644 index 0000000..f0a8f1a Binary files /dev/null and b/app/src/main/res/drawable/no_wifi_pic.png differ diff --git a/app/src/main/res/drawable/splash_1.xml b/app/src/main/res/drawable/splash_1.xml new file mode 100644 index 0000000..e76c5fd --- /dev/null +++ b/app/src/main/res/drawable/splash_1.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/splash_2.xml b/app/src/main/res/drawable/splash_2.xml new file mode 100644 index 0000000..95e9883 --- /dev/null +++ b/app/src/main/res/drawable/splash_2.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/splash_3.xml b/app/src/main/res/drawable/splash_3.xml new file mode 100644 index 0000000..fd4e2a1 --- /dev/null +++ b/app/src/main/res/drawable/splash_3.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/layout-land/fragment_user.xml b/app/src/main/res/layout-land/fragment_user.xml index 1c946e9..c1fa1e2 100644 --- a/app/src/main/res/layout-land/fragment_user.xml +++ b/app/src/main/res/layout-land/fragment_user.xml @@ -14,6 +14,7 @@ android:layout_height="match_parent"> + android:textColor="@color/warn_button_color" + android:visibility="gone" + tools:visibility="visible" /> + +