Merge remote-tracking branch 'origin/master'

This commit is contained in:
mariamusinkina 2025-02-20 20:14:54 +05:00
commit 46616380aa
73 changed files with 1604 additions and 356 deletions

View File

@ -5,6 +5,11 @@ plugins {
kotlinAnnotationProcessor kotlinAnnotationProcessor
id("com.google.dagger.hilt.android").version("2.51.1") 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" val packageName = "ru.myitschool.work"
@ -29,15 +34,37 @@ android {
targetCompatibility = Version.Kotlin.javaSource targetCompatibility = Version.Kotlin.javaSource
} }
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
kotlinOptions { kotlinOptions {
jvmTarget = Version.Kotlin.jvmTarget jvmTarget = Version.Kotlin.jvmTarget
} }
} }
dependencies { 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() 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") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val ktorClientCore = "3.0.3" val ktorClientCore = "3.0.3"
@ -54,9 +81,6 @@ dependencies {
implementation(Dependencies.AndroidX.Navigation.fragment) implementation(Dependencies.AndroidX.Navigation.fragment)
implementation(Dependencies.AndroidX.Navigation.navigationUi) implementation(Dependencies.AndroidX.Navigation.navigationUi)
implementation(Dependencies.Retrofit.library)
implementation(Dependencies.Retrofit.gsonConverter)
implementation("com.squareup.picasso:picasso:2.8") implementation("com.squareup.picasso:picasso:2.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("androidx.datastore:datastore-preferences:1.1.2") implementation("androidx.datastore:datastore-preferences:1.1.2")

View File

@ -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<LoginFragment>()
@Test
fun onSwipeDataRefreshes() {
onView(withId(R.id.refresh)).perform(swipeDown())
onView(withId(R.id.refresh)).check(matches(isRefreshing()))
}
}

View File

@ -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<View> {
return object : BoundedMatcher<View, SwipeRefreshLayout>(
SwipeRefreshLayout::class.java) {
override fun describeTo(description: Description) {
description.appendText("is refreshing")
}
override fun matchesSafely(view: SwipeRefreshLayout): Boolean {
return view.isRefreshing
}
}
}
}

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".App" android:name=".App"

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,6 +1,6 @@
package ru.myitschool.work.core package ru.myitschool.work.core
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ // БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
object Constants { object Constants {
const val SERVER_ADDRESS = "http://192.168.1.103:8080" const val SERVER_ADDRESS = "http://10.6.66.74:8080"
const val TOKEN_KEY = "token" const val TOKEN_KEY = "token"
} }

View File

@ -0,0 +1,18 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.AdminNetworkDataSource
import ru.myitschool.work.domain.admin.AdminRepository
class AdminRepositoryImpl(
private val networkDataSource: AdminNetworkDataSource,
private val localCredentialsLocalDataSource: CredentialsLocalDataSource
) : AdminRepository {
override suspend fun blockUser(login: String): Result<Unit> {
return networkDataSource.blockUser(login, localCredentialsLocalDataSource.getToken())
}
override suspend fun unblockUser(login: String): Result<Unit> {
return networkDataSource.unblockUser(login, localCredentialsLocalDataSource.getToken())
}
}

View File

@ -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<List<PassEntity>> {
return map(
networkDataSource.getCurrentPasses(
pageNum,
pageSize,
credentialsLocalDataSource.getToken()
)
)
}
override suspend fun getUsersPasses(
pageNum: Int,
pageSize: Int,
login: String
): Result<List<PassEntity>> {
return map(
networkDataSource.getUsersPasses(
login = login,
pageNum = pageNum,
pageSize = pageSize,
token = credentialsLocalDataSource.getToken()
)
)
}
private fun map(listDto: Result<List<PassDto>>): Result<List<PassEntity>> {
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
)
}
}
}
}

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.data package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource 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.entities.QrEntity
import ru.myitschool.work.domain.qr.QrRepository import ru.myitschool.work.domain.qr.QrRepository

View File

@ -1,6 +1,5 @@
package ru.myitschool.work.data package ru.myitschool.work.data
import android.util.Log
import ru.myitschool.work.data.dto.UserDto import ru.myitschool.work.data.dto.UserDto
import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource import ru.myitschool.work.data.local.UserLocalDataSource
@ -19,16 +18,29 @@ class UserRepositoryImpl(
return runCatching { return runCatching {
networkDataSource.login(credentialsLocalDataSource.updateToken(login, password)) networkDataSource.login(credentialsLocalDataSource.updateToken(login, password))
.onSuccess { dto -> .fold(
map(dto).onSuccess { userLocalDataSource.cacheData(it) } onSuccess = { dto ->
} map(dto).fold(
onSuccess = {
userLocalDataSource.cacheData(it)
},
onFailure = { error(it) }
)
},
onFailure = { error(it) }
)
} }
} }
override suspend fun authorize(token: String): Result<Unit> { override suspend fun authorize(token: String): Result<Unit> {
return networkDataSource.login(token).fold( return networkDataSource.login(token).fold(
onSuccess = { Result.success(Unit) }, onSuccess = { dto ->
onFailure = { error -> Result.failure(error) } 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()!! return userLocalDataSource.getUser()!!
} }
private fun map(userDto: UserDto): Result<UserEntity> { private suspend fun map(userDto: UserDto): Result<UserEntity> {
return runCatching { return runCatching {
UserEntity( UserEntity(
id = userDto.id ?: error("Null user id"),
name = userDto.name ?: error("Null user name"), name = userDto.name ?: error("Null user name"),
lastVisit = userDto.lastVisit ?: error("Null user lastVisit"), lastVisit = userDto.lastVisit ?: "",
photoUrl = userDto.photoUrl ?: error("Null user photoUrl"), 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<UserEntity> {
return networkDataSource.getUserByLogin(login, credentialsLocalDataSource.getToken()).fold(
onSuccess = { map(it) }, onFailure = { Result.failure(it) }
)
}
} }

View File

@ -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?
)

View File

@ -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;
}

View File

@ -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?
)

View File

@ -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<Unit> =
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<Unit> =
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
}
}
}

View File

@ -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<List<PassDto>> =
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<List<PassDto>> = 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()
}
}
}

View File

@ -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.headers
import io.ktor.client.request.patch import io.ktor.client.request.patch
@ -11,28 +11,25 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.QrDto import ru.myitschool.work.data.dto.QrDto
import ru.myitschool.work.data.network.KtorClient
import ru.myitschool.work.domain.entities.QrEntity import ru.myitschool.work.domain.entities.QrEntity
object QrNetworkDataSource { object QrNetworkDataSource {
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> = withContext(Dispatchers.IO) { suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching { 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)
)
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
} }
}
} }

View File

@ -22,16 +22,45 @@ object UserNetworkDataSource {
suspend fun login(token: String): Result<UserDto> = suspend fun login(token: String): Result<UserDto> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
runCatching { runCatching {
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") { val result =
headers { KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") {
append(HttpHeaders.Authorization, token) headers {
append(HttpHeaders.Authorization, token)
}
} }
}
if (result.status != HttpStatusCode.OK) if (result.status != HttpStatusCode.OK)
error("Status ${result.status}") error("Status ${result.status}")
result.body() result.body()
} }
} }
suspend fun getUserByLogin() {} suspend fun isRoleHasAdminPermissions(roleId: Int, token: String): Result<Boolean> =
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<UserDto> =
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()
}
}
} }

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.domain.admin
interface AdminRepository {
suspend fun blockUser(login: String): Result<Unit>
suspend fun unblockUser(login: String): Result<Unit>
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.entities
data class PassEntity(
val type: String,
val name: String,
val time: String
)

View File

@ -1,9 +1,10 @@
package ru.myitschool.work.domain.entities package ru.myitschool.work.domain.entities
data class UserEntity( data class UserEntity(
val id: String, val isAdmin: Boolean,
val name: String, val name: String,
val lastVisit: String, val lastVisit: String,
val photoUrl: String, val photoUrl: String,
val position: String, val position: String,
val isCardBlocked: Boolean
) )

View File

@ -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<List<PassEntity>> =
repository.getCurrentPasses(pageNum, pageSize)
}

View File

@ -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)
}

View File

@ -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<List<PassEntity>>
suspend fun getUsersPasses(pageNum: Int, pageSize: Int, login: String): Result<List<PassEntity>>
}

View File

@ -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<UserEntity> = repository.getUserByLogin(login)
}

View File

@ -5,4 +5,5 @@ import ru.myitschool.work.domain.entities.UserEntity
interface UserRepository { interface UserRepository {
suspend fun getCurrentUser(): UserEntity suspend fun getCurrentUser(): UserEntity
suspend fun getUserByLogin(login: String) : Result<UserEntity>
} }

View File

@ -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()
}
}

View File

@ -4,13 +4,18 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.utils.isOnline
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint @AndroidEntryPoint
class RootActivity : AppCompatActivity() { class RootActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_root) setContentView(R.layout.activity_root)
if (!isOnline(this)) {
val dialog = NoInternetNotificationFragment()
dialog.isCancelable = false
dialog.show(supportFragmentManager, "NO_INTERNET")
}
} }
} }

View File

@ -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> { 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()
}
}

View File

@ -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>(State.Waiting)
val state = _state.asStateFlow()
// private var _listState: Flow<PagingData<PassEntity>>? = null
// val listState: Flow<PagingData<PassEntity>> 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 <T : ViewModel> create(modelClass: Class<T>, 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
}
}
}
}

View File

@ -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<List<PassEntity>>,
private val login: String
) : PagingSource<Int, PassEntity>() {
override fun getRefreshKey(state: PagingState<Int, PassEntity>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PassEntity> {
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) }
)
}
}

View File

@ -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> { 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()
}
}

View File

@ -1,8 +1,9 @@
package ru.myitschool.work.ui.login package ru.myitschool.work.ui.login
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -32,11 +33,18 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
binding.error.visibleOrGone(state is LoginViewModel.State.Error) binding.error.visibleOrGone(state is LoginViewModel.State.Error)
when (state) { when (state) {
is LoginViewModel.State.Error -> binding.error.text = state.errorMessage is LoginViewModel.State.Error -> {
is LoginViewModel.State.Loading -> Unit binding.error.text = state.errorMessage
setButtonActive()
}
is LoginViewModel.State.Loading -> setButtonInactive()
is LoginViewModel.State.Waiting -> Unit is LoginViewModel.State.Waiting -> Unit
is LoginViewModel.State.LoginCheckCompleted -> binding.login.isEnabled = is LoginViewModel.State.LoginCheckCompleted -> {
state.isCompleted 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() { override fun onDestroy() {
_binding = null _binding = null
super.onDestroy() super.onDestroy()

View File

@ -9,14 +9,10 @@ import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.databinding.FragmentSplashBinding
import ru.myitschool.work.utils.collectWithLifecycle import ru.myitschool.work.utils.collectWithLifecycle
class SplashFragment : Fragment(R.layout.fragment_splash) { class SplashFragment : Fragment(R.layout.fragment_splash) {
private var _binding: FragmentSplashBinding? = null
private val binding: FragmentSplashBinding get() = _binding!!
private val viewModel by viewModels<LoginViewModel> { LoginViewModel.Factory } private val viewModel by viewModels<LoginViewModel> { LoginViewModel.Factory }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -30,21 +26,15 @@ class SplashFragment : Fragment(R.layout.fragment_splash) {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentSplashBinding.bind(view)
viewModel.action.collectWithLifecycle(this) { action -> viewModel.action.collectWithLifecycle(this) { action ->
val navController = findNavController() val navController = findNavController()
when (action) { when (action) {
is LoginViewModel.Action.GoToLogin -> navController.navigate(R.id.loginFragment) is LoginViewModel.Action.GoToLogin -> navController.navigate(R.id.loginFragment)
is LoginViewModel.Action.OpenApp -> 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()
}
} }

View File

@ -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<PassEntity, PassesListAdapter.ViewHolder>(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<PassEntity>() {
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))
}
}

View File

@ -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<List<PassEntity>>
) : PagingSource<Int, PassEntity>() {
override fun getRefreshKey(state: PagingState<Int, PassEntity>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PassEntity> {
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) }
)
}
}

View File

@ -1,17 +1,21 @@
package ru.myitschool.work.ui.profile package ru.myitschool.work.ui.profile
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentUserBinding 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.collectWithLifecycle
import ru.myitschool.work.utils.visibleOrGone import ru.myitschool.work.utils.visibleOrGone
class UserFragment : Fragment(R.layout.fragment_user) { class UserFragment : Fragment(R.layout.fragment_user) {
private var _binding: FragmentUserBinding? = null private var _binding: FragmentUserBinding? = null
@ -22,30 +26,59 @@ class UserFragment : Fragment(R.layout.fragment_user) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentUserBinding.bind(view) _binding = FragmentUserBinding.bind(view)
val adapter = PassesListAdapter()
binding.passes.adapter = adapter
viewModel.state.collectWithLifecycle(this) { state -> viewModel.state.collectWithLifecycle(this) { state ->
(binding.refresh as SwipeRefreshLayout).isRefreshing = binding.refresh.isRefreshing = state is UserViewModel.State.Loading
state is UserViewModel.State.Loading binding.content.visibleOrGone(state is UserViewModel.State.Show)
binding.content?.visibleOrGone(state is UserViewModel.State.Show)
when (state) { when (state) {
is UserViewModel.State.Loading -> Unit is UserViewModel.State.Loading -> Unit
is UserViewModel.State.Show -> { is UserViewModel.State.Show -> {
val user = state.userEntity val user = state.userEntity
adapter.submitList(state.passes)
binding.scan.visibleOrGone(!user.isCardBlocked)
binding.findUser.visibleOrGone(user.isAdmin)
binding.fullname.text = user.name binding.fullname.text = user.name
binding.position.text = user.position binding.position.text = user.position
binding.lastEntry.text = user.lastVisit binding.lastEntry.text = user.lastVisit
Picasso.get().load(user.photoUrl).into(binding.photo) 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() { override fun onDestroy() {

View File

@ -1,35 +1,63 @@
package ru.myitschool.work.ui.profile package ru.myitschool.work.ui.profile
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.PassRepositoryImpl
import ru.myitschool.work.data.UserRepositoryImpl import ru.myitschool.work.data.UserRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource 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.data.network.UserNetworkDataSource
import ru.myitschool.work.domain.entities.PassEntity
import ru.myitschool.work.domain.entities.UserEntity import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.domain.login.LogoutUseCase import ru.myitschool.work.domain.login.LogoutUseCase
import ru.myitschool.work.domain.passes.GetCurrentPassesUseCase
import ru.myitschool.work.domain.user.GetCurrentUserUseCase import ru.myitschool.work.domain.user.GetCurrentUserUseCase
class UserViewModel( class UserViewModel(
private val getCurrentUserUseCase: GetCurrentUserUseCase, private val getCurrentUserUseCase: GetCurrentUserUseCase,
private val logoutUseCase: LogoutUseCase private val logoutUseCase: LogoutUseCase,
private val getCurrentPassesUseCase: GetCurrentPassesUseCase
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<State>(State.Loading) private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow() val state = _state.asStateFlow()
val listState = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 50
)
) {
PassesPagingSource(getCurrentPassesUseCase::invoke)
}.flow
.cachedIn(viewModelScope)
init { init {
updateState() updateState()
} }
private fun updateState() { private fun updateState() {
viewModelScope.launch { 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 { sealed interface State {
data object Loading : State data object Loading : State
data class Show(val userEntity: UserEntity) : State data class Show(val userEntity: UserEntity, val passes: List<PassEntity>) : State
} }
companion object { companion object {
@ -57,7 +85,13 @@ class UserViewModel(
) )
return UserViewModel( return UserViewModel(
getCurrentUserUseCase = GetCurrentUserUseCase(repository = repository), getCurrentUserUseCase = GetCurrentUserUseCase(repository = repository),
logoutUseCase = LogoutUseCase(repository = repository) logoutUseCase = LogoutUseCase(repository = repository),
getCurrentPassesUseCase = GetCurrentPassesUseCase(
repository = PassRepositoryImpl(
networkDataSource = PassNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
)
) as T ) as T
} }
} }

View File

@ -8,7 +8,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentQrResultBinding 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.ui.qr.scan.QrScanDestination.getDataIfExist
import ru.myitschool.work.utils.collectWithLifecycle import ru.myitschool.work.utils.collectWithLifecycle
@ -20,25 +20,18 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
private var _binding: FragmentQrResultBinding? = null private var _binding: FragmentQrResultBinding? = null
private val binding: FragmentQrResultBinding get() = _binding!! private val binding: FragmentQrResultBinding get() = _binding!!
private var _resultQr: String? = null
private val resultQr: String = _resultQr!!
private val viewModel by viewModels<QrResultViewModel> { QrResultViewModel.Factory } private val viewModel by viewModels<QrResultViewModel> { QrResultViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentQrResultBinding.bind(view) _binding = FragmentQrResultBinding.bind(view)
if (savedInstanceState != null) {
_resultQr = savedInstanceState.getString(QrScanDestination.REQUEST_KEY)
}
parentFragmentManager.setFragmentResultListener(RESPONSE_KEY, this) { _, result -> parentFragmentManager.setFragmentResultListener(RESPONSE_KEY, this) { _, result ->
_resultQr = getDataIfExist(result) getDataIfExist(result)?.let { viewModel.setQr(QrEntity(it)) }
viewModel.update(resultQr) viewModel.update()
} }
viewModel.state.collectWithLifecycle(this) { state -> viewModel.state.collectWithLifecycle(this) { state ->
if (_resultQr == null) { if (viewModel._qrEntity == null) {
binding.result.setText(R.string.door_closed) binding.result.setText(R.string.door_closed)
binding.close.background = binding.close.background =
ContextCompat.getDrawable(requireContext(), R.drawable.warn_button) 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() { override fun onDestroy() {
_binding = null _binding = null
super.onDestroy() super.onDestroy()

View File

@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch 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.QrRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.domain.entities.QrEntity import ru.myitschool.work.domain.entities.QrEntity
@ -20,9 +20,16 @@ class QrResultViewModel(
private val _state = MutableStateFlow<State>(State.Loading) private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow() 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 { viewModelScope.launch {
pushQrUseCase(QrEntity(code = qrValue)).fold( pushQrUseCase(qrEntity).fold(
onSuccess = { _state.emit(State.Show) }, onSuccess = { _state.emit(State.Show) },
onFailure = { _state.emit(State.Error(it.message.toString())) } onFailure = { _state.emit(State.Error(it.message.toString())) }
) )

View File

@ -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
}

View File

@ -1,18 +1,19 @@
package ru.myitschool.work.utils package ru.myitschool.work.utils
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
inline fun <T> Flow<T>.collectWithLifecycle( fun <T> Flow<T>.collectWithLifecycle(
fragment: Fragment, fragment: Fragment,
crossinline collector: (T) -> Unit function: suspend (T) -> Unit
) { ) {
fragment.viewLifecycleOwner.lifecycleScope.launch { fragment.viewLifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
collector(value) collect { function.invoke(it) }
} }
} }
} }

View File

@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector
android:width="108dp" android:height="108dp"
android:height="108dp" android:width="108dp"
android:viewportWidth="108" android:viewportHeight="108"
android:viewportHeight="108"> android:viewportWidth="108"
<path xmlns:android="http://schemas.android.com/apk/res/android">
android:fillColor="#3DDC84" <path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" /> android:pathData="M0,0h108v108h-108z"/>
<path <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M9,0L9,108" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:pathData="M19,0L19,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M29,0L29,108" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:pathData="M39,0L39,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M49,0L49,108" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:pathData="M59,0L59,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M69,0L69,108" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:pathData="M79,0L79,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M89,0L89,108" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:pathData="M99,0L99,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,9L108,9" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:pathData="M0,19L108,19" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,29L108,29" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> </vector>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -1,30 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp"
android:width="108dp" android:height="108dp"
android:height="108dp" android:viewportWidth="221"
android:viewportWidth="108" android:viewportHeight="221">
android:viewportHeight="108"> <group>
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> <clip-path
<aapt:attr name="android:fillColor"> android:pathData="M0,0h221v221h-221z"/>
<gradient <path
android:endX="85.84757" android:pathData="M0,0h221v221h-221z"
android:endY="92.4963" android:fillColor="#001A8C"/>
android:startX="42.9492" <path
android:startY="49.59793" android:pathData="M66.3,0.4V19.58H39.06L39.06,81.83H66.3L66.3,101H-7.2V81.83H19.89L19.89,19.58H-7.2V0.4H66.3ZM100.84,77.79V19.58H92.92L92.92,101H73.75L73.75,23.61C73.75,10.73 84.08,0.4 96.96,0.4C103.09,0.4 108.99,2.81 113.26,7.24C117.61,11.5 120.01,17.48 120.01,23.61V81.83H128.09V0.4H147.26V77.79C147.26,90.68 136.78,101 124.05,101C117.92,101 112.02,98.59 107.67,94.17C103.4,89.9 100.84,83.92 100.84,77.79ZM177.99,73.91C165.11,73.91 154.71,63.43 154.71,50.62V0.4H173.88V54.74H177.68C183.89,54.74 189.79,57.14 194.21,61.49C198.56,65.92 200.97,71.82 200.97,78.02V81.83H209.04V0.4H228.21V77.79C228.21,90.68 217.73,101 205,101C192.27,101 181.8,90.68 181.8,77.79V73.91H177.99ZM-51.58,175.74H-20.45V167.67H-51.58C-64.46,167.67 -74.79,157.34 -74.79,144.61C-74.79,131.73 -64.46,121.4 -51.58,121.4H25.81L25.81,140.58H-55.61V148.49H-24.49C-11.76,148.49 -1.28,158.97 -1.28,171.7C-1.28,184.43 -11.76,194.91 -24.49,194.91H-55.61V202.83H25.81V222H-51.58C-64.46,222 -74.79,211.68 -74.79,198.79C-74.79,186.06 -64.46,175.74 -51.58,175.74ZM52.45,140.58V222H33.27V144.69C33.27,131.81 43.68,121.4 56.56,121.4H83.49C96.3,121.4 106.78,131.81 106.78,144.69V171.63C106.78,184.43 96.3,194.91 83.49,194.91H79.54V202.83H106.78V222H83.57C77.44,222 71.54,219.59 67.19,215.17C62.93,210.9 60.36,204.92 60.36,198.79C60.36,186.06 70.84,175.74 83.57,175.74H87.61V140.58H52.45ZM187.74,198.79C187.74,204.92 185.17,210.9 180.9,215.17C176.56,219.59 170.66,222 164.53,222H114.23V202.83H168.56V194.91H164.6C158.47,194.91 152.57,192.35 148.23,188C143.88,183.66 141.32,177.76 141.32,171.63V167.67H137.44C131.31,167.67 125.33,165.26 121.06,160.91C116.64,156.64 114.23,150.67 114.23,144.61C114.23,138.48 116.64,132.51 121.06,128.24C125.33,123.81 131.31,121.4 137.44,121.4H187.74V140.58H133.4V148.49H137.21C143.41,148.49 149.31,151.06 153.74,155.4C158.09,159.75 160.49,165.65 160.49,171.78V175.74H164.53C170.66,175.74 176.56,178.15 180.9,182.49C185.17,186.76 187.74,192.74 187.74,198.79ZM218.39,175.74H249.52V167.67H218.39C205.51,167.67 195.18,157.34 195.18,144.61C195.18,131.73 205.51,121.4 218.39,121.4H295.78V140.58H214.36V148.49H245.48C258.21,148.49 268.69,158.97 268.69,171.7C268.69,184.43 258.21,194.91 245.48,194.91H214.36V202.83H295.78V222H218.39C205.51,222 195.18,211.68 195.18,198.79C195.18,186.06 205.51,175.74 218.39,175.74Z"
android:type="linear"> android:strokeAlpha="0.3"
<item android:fillColor="#ffffff"
android:color="#44000000" android:fillAlpha="0.3"/>
android:offset="0.0" /> <path
<item android:pathData="M109.12,53C141.65,53 168,78.74 168,110.5C168,142.26 141.64,168 109.12,168C90.77,168 73.8,159.74 62.72,145.88C62.41,145.5 62.18,145.05 62.04,144.58C61.91,144.1 61.87,143.6 61.93,143.11C61.99,142.61 62.15,142.14 62.4,141.71C62.65,141.28 62.98,140.9 63.38,140.61C64.19,139.99 65.2,139.71 66.21,139.84C67.22,139.96 68.14,140.47 68.78,141.26C73.63,147.28 79.78,152.14 86.76,155.47C93.75,158.8 101.39,160.51 109.13,160.48C137.39,160.48 160.3,138.1 160.3,110.5C160.3,82.9 137.39,60.52 109.13,60.52C101.5,60.49 93.96,62.15 87.06,65.39C80.15,68.63 74.05,73.36 69.2,79.24C68.55,80.02 67.62,80.52 66.61,80.63C65.6,80.73 64.58,80.44 63.78,79.82C63.39,79.51 63.06,79.14 62.82,78.7C62.57,78.27 62.42,77.79 62.36,77.3C62.31,76.81 62.35,76.31 62.49,75.83C62.64,75.35 62.87,74.91 63.19,74.53C74.29,61.02 91.03,53 109.12,53ZM112.26,92.46L127.82,108.02C129.33,109.53 129.36,111.93 127.89,113.4L112.68,128.61C112.33,128.96 111.9,129.24 111.44,129.43C110.98,129.62 110.48,129.71 109.98,129.7C109.48,129.7 108.98,129.59 108.52,129.39C108.06,129.2 107.65,128.91 107.3,128.55C106.94,128.2 106.65,127.78 106.45,127.32C106.26,126.86 106.15,126.37 106.15,125.87C106.14,125.37 106.24,124.87 106.42,124.4C106.61,123.94 106.89,123.52 107.24,123.16L116.14,114.26L56.85,114.27C56.35,114.27 55.85,114.18 55.39,114C54.92,113.81 54.5,113.54 54.14,113.18C53.78,112.83 53.5,112.42 53.3,111.96C53.11,111.5 53,111 53,110.5C53,108.42 54.72,106.74 56.85,106.74H115.89L106.93,97.78C106.58,97.43 106.29,97.01 106.09,96.55C105.89,96.09 105.79,95.59 105.78,95.09C105.78,94.59 105.87,94.1 106.06,93.63C106.25,93.17 106.53,92.74 106.88,92.39C107.23,92.04 107.66,91.76 108.12,91.58C108.58,91.39 109.08,91.3 109.58,91.3C110.08,91.31 110.57,91.41 111.03,91.61C111.49,91.81 111.9,92.09 112.25,92.45"
android:color="#00000000" android:fillColor="#ffffff"/>
android:offset="1.0" /> <path
</gradient> android:pathData="M112.26,92.46L127.82,108.02C129.33,109.53 129.36,111.93 127.89,113.4L112.68,128.61C112.33,128.96 111.9,129.24 111.44,129.43C110.98,129.62 110.48,129.71 109.98,129.7C109.48,129.7 108.98,129.59 108.52,129.39C108.06,129.2 107.65,128.91 107.3,128.55C106.94,128.2 106.65,127.78 106.45,127.32C106.26,126.86 106.15,126.37 106.15,125.87C106.14,125.37 106.24,124.87 106.42,124.4C106.61,123.94 106.89,123.52 107.24,123.16L116.14,114.26L56.85,114.27C56.35,114.27 55.85,114.18 55.39,114C54.92,113.81 54.5,113.54 54.14,113.18C53.78,112.83 53.5,112.42 53.3,111.96C53.11,111.5 53,111 53,110.5C53,108.42 54.72,106.74 56.85,106.74H115.89L106.93,97.78C106.58,97.43 106.29,97.01 106.09,96.55C105.89,96.09 105.79,95.59 105.78,95.09C105.78,94.59 105.87,94.1 106.06,93.63C106.25,93.17 106.53,92.74 106.88,92.39C107.23,92.04 107.66,91.76 108.12,91.58C108.58,91.39 109.08,91.3 109.58,91.3C110.08,91.31 110.57,91.41 111.03,91.61C111.49,91.81 111.9,92.09 112.25,92.45M109.12,53C141.65,53 168,78.74 168,110.5C168,142.26 141.64,168 109.12,168C90.77,168 73.8,159.74 62.72,145.88C62.41,145.5 62.18,145.05 62.04,144.58C61.91,144.1 61.87,143.6 61.93,143.11C61.99,142.61 62.15,142.14 62.4,141.71C62.65,141.28 62.98,140.9 63.38,140.61C64.19,139.99 65.2,139.71 66.21,139.84C67.22,139.96 68.14,140.47 68.78,141.26C73.63,147.28 79.78,152.14 86.76,155.47C93.75,158.8 101.39,160.51 109.13,160.48C137.39,160.48 160.3,138.1 160.3,110.5C160.3,82.9 137.39,60.52 109.13,60.52C101.5,60.49 93.96,62.15 87.06,65.39C80.15,68.63 74.05,73.36 69.2,79.24C68.55,80.02 67.62,80.52 66.61,80.63C65.6,80.73 64.58,80.44 63.78,79.82C63.39,79.51 63.06,79.14 62.82,78.7C62.57,78.27 62.42,77.79 62.36,77.3C62.31,76.81 62.35,76.31 62.49,75.83C62.64,75.35 62.87,74.91 63.19,74.53C74.29,61.02 91.03,53 109.12,53Z"
</aapt:attr> android:strokeWidth="2"
</path> android:fillColor="#00000000"
<path android:strokeColor="#ffffff"/>
android:fillColor="#FFFFFF" </group>
android:fillType="nonZero" </vector>
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="655dp"
android:viewportWidth="375"
android:viewportHeight="655">
<path
android:pathData="M102.93,0.51V37.2H50.8L50.8,156.31H102.93V193H-37.72V156.31H14.11L14.11,37.2H-37.72V0.51L102.93,0.51ZM169.02,148.59L169.02,37.2H153.87L153.87,193H117.18L117.18,44.92C117.18,20.27 136.94,0.51 161.59,0.51C173.32,0.51 184.61,5.12 192.78,13.58C201.1,21.75 205.7,33.19 205.7,44.92L205.7,156.31H221.15V0.51H257.84V148.59C257.84,173.25 237.79,193 213.43,193C201.69,193 190.41,188.4 182.09,179.93C173.92,171.76 169.02,160.32 169.02,148.59ZM316.65,141.16C291.99,141.16 272.09,121.11 272.09,96.61V0.51L308.77,0.51V104.48H316.05C327.93,104.48 339.22,109.08 347.69,117.4C356.01,125.87 360.61,137.15 360.61,149.04V156.31H376.06V0.51H412.74V148.59C412.74,173.25 392.69,193 368.33,193C343.98,193 323.92,173.25 323.92,148.59V141.16H316.65ZM58.2,335.48H117.76V320.03H58.2C33.55,320.03 13.8,300.28 13.8,275.92C13.8,251.26 33.55,231.51 58.2,231.51H206.28V268.2H50.48V283.35H110.04C134.4,283.35 154.45,303.4 154.45,327.76C154.45,352.11 134.4,372.17 110.04,372.17H50.48V387.31H206.28V424H58.2C33.55,424 13.8,404.25 13.8,379.59C13.8,355.23 33.55,335.48 58.2,335.48ZM257.26,268.2V424H220.57V276.07C220.57,251.41 240.47,231.51 265.13,231.51H316.67C341.17,231.51 361.22,251.41 361.22,276.07V327.61C361.22,352.11 341.17,372.17 316.67,372.17H309.09V387.31H361.22V424H316.81C305.08,424 293.79,419.4 285.48,410.93C277.31,402.76 272.4,391.33 272.4,379.59C272.4,355.23 292.46,335.48 316.81,335.48H324.54V268.2H257.26ZM154.45,610.59C154.45,622.33 149.55,633.76 141.38,641.93C133.06,650.4 121.77,655 110.04,655H13.8L13.8,618.31H117.76V603.16H110.19C98.45,603.16 87.17,598.26 78.85,589.95C70.53,581.63 65.63,570.34 65.63,558.61V551.03H58.2C46.47,551.03 35.03,546.43 26.87,538.11C18.4,529.94 13.8,518.51 13.8,506.92C13.8,495.19 18.4,483.75 26.87,475.58C35.03,467.12 46.47,462.51 58.2,462.51H154.45L154.45,499.2H50.48V514.35H57.76C69.64,514.35 80.93,519.25 89.39,527.57C97.71,535.88 102.32,547.17 102.32,558.9V566.48H110.04C121.77,566.48 133.06,571.08 141.38,579.4C149.55,587.57 154.45,599.01 154.45,610.59ZM213.11,566.48H272.67V551.03H213.11C188.46,551.03 168.7,531.28 168.7,506.92C168.7,482.27 188.46,462.51 213.11,462.51H361.19V499.2H205.39V514.35H264.95C289.3,514.35 309.36,534.4 309.36,558.76C309.36,583.11 289.3,603.16 264.95,603.16H205.39V618.31H361.19V655H213.11C188.46,655 168.7,635.25 168.7,610.59C168.7,586.23 188.46,566.48 213.11,566.48Z"
android:strokeAlpha="0.3"
android:fillColor="#ffffff"
android:fillAlpha="0.3"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="117dp"
android:height="117dp"
android:viewportWidth="117"
android:viewportHeight="117">
<path
android:pathData="M57.12,1C89.65,1 116,26.74 116,58.5C116,90.26 89.64,116 57.12,116C38.77,116 21.8,107.74 10.72,93.89C10.41,93.5 10.18,93.05 10.04,92.58C9.91,92.1 9.87,91.6 9.93,91.11C9.99,90.61 10.15,90.14 10.4,89.71C10.65,89.28 10.98,88.91 11.38,88.61C12.19,87.99 13.2,87.72 14.21,87.84C15.22,87.96 16.14,88.47 16.78,89.26C21.63,95.28 27.78,100.14 34.76,103.47C41.75,106.8 49.39,108.51 57.13,108.48C85.39,108.48 108.3,86.1 108.3,58.5C108.3,30.9 85.39,8.52 57.13,8.52C49.5,8.49 41.96,10.15 35.06,13.39C28.15,16.63 22.05,21.36 17.2,27.24C16.55,28.02 15.62,28.52 14.61,28.63C13.6,28.73 12.58,28.44 11.78,27.82C11.39,27.51 11.06,27.14 10.82,26.7C10.57,26.27 10.42,25.79 10.36,25.3C10.31,24.81 10.35,24.31 10.49,23.83C10.64,23.35 10.87,22.91 11.19,22.53C22.29,9.02 39.03,1 57.12,1ZM60.26,40.46L75.82,56.02C77.33,57.53 77.36,59.93 75.89,61.4L60.69,76.61C60.33,76.96 59.91,77.24 59.44,77.43C58.98,77.62 58.48,77.71 57.98,77.7C57.48,77.7 56.98,77.59 56.52,77.39C56.06,77.2 55.65,76.91 55.3,76.55C54.94,76.2 54.65,75.78 54.45,75.32C54.26,74.86 54.15,74.37 54.15,73.87C54.14,73.37 54.23,72.87 54.42,72.41C54.61,71.94 54.89,71.52 55.24,71.16L64.14,62.26L4.85,62.27C4.35,62.27 3.85,62.18 3.39,62C2.92,61.81 2.5,61.54 2.14,61.19C1.78,60.84 1.5,60.42 1.3,59.96C1.11,59.5 1,59 1,58.5C1,56.42 2.72,54.74 4.85,54.74H63.89L54.94,45.78C54.58,45.43 54.29,45.01 54.09,44.55C53.89,44.09 53.79,43.59 53.78,43.09C53.78,42.59 53.87,42.1 54.06,41.63C54.25,41.17 54.53,40.74 54.88,40.39C55.23,40.04 55.66,39.76 56.12,39.58C56.58,39.39 57.08,39.3 57.58,39.3C58.08,39.31 58.57,39.41 59.03,39.61C59.49,39.81 59.91,40.09 60.25,40.45"
android:fillColor="#ffffff"/>
<path
android:pathData="M60.26,40.46L75.82,56.02C77.33,57.53 77.36,59.93 75.89,61.4L60.69,76.61C60.33,76.96 59.91,77.24 59.44,77.43C58.98,77.62 58.48,77.71 57.98,77.7C57.48,77.7 56.98,77.59 56.52,77.39C56.06,77.2 55.65,76.91 55.3,76.55C54.94,76.2 54.65,75.78 54.45,75.32C54.26,74.86 54.15,74.37 54.15,73.87C54.14,73.37 54.23,72.87 54.42,72.41C54.61,71.94 54.89,71.52 55.24,71.16L64.14,62.26L4.85,62.27C4.35,62.27 3.85,62.18 3.39,62C2.92,61.81 2.5,61.54 2.14,61.19C1.78,60.84 1.5,60.42 1.3,59.96C1.11,59.5 1,59 1,58.5C1,56.42 2.72,54.74 4.85,54.74H63.89L54.94,45.78C54.58,45.43 54.29,45.01 54.09,44.55C53.89,44.09 53.79,43.59 53.78,43.09C53.78,42.59 53.87,42.1 54.06,41.63C54.25,41.17 54.53,40.74 54.88,40.39C55.23,40.04 55.66,39.76 56.12,39.58C56.58,39.39 57.08,39.3 57.58,39.3C58.08,39.31 58.57,39.41 59.03,39.61C59.49,39.81 59.91,40.09 60.25,40.45M57.12,1C89.65,1 116,26.74 116,58.5C116,90.26 89.64,116 57.12,116C38.77,116 21.8,107.74 10.72,93.89C10.41,93.5 10.18,93.05 10.04,92.58C9.91,92.1 9.87,91.6 9.93,91.11C9.99,90.61 10.15,90.14 10.4,89.71C10.65,89.28 10.98,88.91 11.38,88.61C12.19,87.99 13.2,87.72 14.21,87.84C15.22,87.96 16.14,88.47 16.78,89.26C21.63,95.28 27.78,100.14 34.76,103.47C41.75,106.8 49.39,108.51 57.13,108.48C85.39,108.48 108.3,86.1 108.3,58.5C108.3,30.9 85.39,8.52 57.13,8.52C49.5,8.49 41.96,10.15 35.06,13.39C28.15,16.63 22.05,21.36 17.2,27.24C16.55,28.02 15.62,28.52 14.61,28.63C13.6,28.73 12.58,28.44 11.78,27.82C11.39,27.51 11.06,27.14 10.82,26.7C10.57,26.27 10.42,25.79 10.36,25.3C10.31,24.81 10.35,24.31 10.49,23.83C10.64,23.35 10.87,22.91 11.19,22.53C22.29,9.02 39.03,1 57.12,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -14,6 +14,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:id="@+id/content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"
@ -132,7 +133,21 @@
android:background="@drawable/warn_button_outline" android:background="@drawable/warn_button_outline"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:text="@string/block_card_button_text" android:text="@string/block_card_button_text"
android:textColor="@color/warn_button_color" /> android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/unblock"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/main_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/unblock_card_button_text"
android:textColor="@color/main_button_color"
android:visibility="gone" />
<ImageButton <ImageButton
android:id="@+id/logout" android:id="@+id/logout"

View File

@ -5,19 +5,19 @@
android:background="@color/white"> android:background="@color/white">
<LinearLayout <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical">
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView <TextView
android:layout_gravity="start"
android:id="@+id/title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:fontFamily="@font/manrope_bold" android:fontFamily="@font/manrope_bold"
android:text="@string/find_employee_header" android:text="@string/find_employee_button_text"
android:textSize="24sp" /> android:textSize="24sp" />
<EditText <EditText
@ -41,6 +41,16 @@
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:text="@string/find_button_text" android:text="@string/find_button_text"
android:textColor="@color/white" /> android:textColor="@color/white" />
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/error"
android:textAlignment="center"
android:textColor="@color/warn_button_color"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8" ?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="20dp"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/no_wifi_pic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/offline_error"
android:textFontWeight="700"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/no_internet_instructions"
android:textFontWeight="400"
android:textSize="18sp" />
<Button
android:foreground="?attr/selectableItemBackground"
android:textColor="@color/white"
android:layout_marginTop="16dp"
android:id="@+id/close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/to_main_menu"
android:background="@drawable/main_button" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
android:id="@+id/result" android:id="@+id/result"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="24dp" android:textSize="24sp"
tools:text="Успешно!"/> tools:text="Успешно!"/>
<Button <Button

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent" >
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,9 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/warn_button_color"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Some error" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh" android:id="@+id/refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -95,15 +107,6 @@
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:text="@string/scan_qr_text" android:text="@string/scan_qr_text"
android:textColor="@color/white" /> android:textColor="@color/white" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:text="Some error" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@ -116,17 +119,31 @@
android:id="@+id/block" android:id="@+id/block"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/warn_button_outline" android:background="@drawable/warn_button_outline"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:text="@string/block_card_button_text" android:text="@string/block_card_button_text"
android:textColor="@color/warn_button_color" /> android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/unblock"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/main_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/unblock_card_button_text"
android:textColor="@color/main_button_color"
android:visibility="gone" />
<ImageButton <ImageButton
android:id="@+id/logout" android:id="@+id/logout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/warn_button" android:background="@drawable/warn_button"
android:padding="12dp" android:padding="12dp"
android:src="@drawable/exit" /> android:src="@drawable/exit" />
@ -143,6 +160,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/passes" android:id="@+id/passes"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:layout_marginHorizontal="20dp"

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -22,6 +22,9 @@
<action <action
android:id="@+id/action_userFragment_to_qrScanFragment" android:id="@+id/action_userFragment_to_qrScanFragment"
app:destination="@id/qrScanFragment" /> app:destination="@id/qrScanFragment" />
<action
android:id="@+id/action_userFragment_to_adminFragment"
app:destination="@id/adminFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/qrScanFragment" android:id="@+id/qrScanFragment"
@ -62,5 +65,24 @@
<action <action
android:id="@+id/action_splashFragment_to_loginFragment" android:id="@+id/action_splashFragment_to_loginFragment"
app:destination="@id/loginFragment" /> app:destination="@id/loginFragment" />
<action
android:id="@+id/action_splashFragment_to_userFragment"
app:destination="@id/userFragment"
app:popUpTo="@id/nav_graph"
app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment
android:id="@+id/adminFragment"
android:name="ru.myitschool.work.ui.admin.search.AdminFragment"
android:label="AdminFragment"
tools:layout="@layout/fragment_find_employee">
<action
android:id="@+id/action_adminFragment_to_viewUserAsAdminFragment"
app:destination="@id/viewUserAsAdminFragment" />
</fragment>
<fragment
android:id="@+id/viewUserAsAdminFragment"
android:name="ru.myitschool.work.ui.admin.view.ViewUserAsAdminFragment"
android:label="ViewUserAsAdminFragment"
tools:layout="@layout/fragment_user" />
</navigation> </navigation>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">NTO Pass</string> <string name="app_name">QR Pass</string>
<string name="scan_qr_text">Scan QR</string> <string name="scan_qr_text">Scan QR</string>
<string name="login_hint">Enter login</string> <string name="login_hint">Enter login</string>
<string name="welcome_text">Welcome to NTO Pass app</string> <string name="welcome_text">Welcome to\nQR Pass app</string>
<string name="login">Login</string> <string name="login">Login</string>
<string name="refresh_data">Refresh</string> <string name="refresh_data">Refresh</string>
<string name="to_main_menu">Close</string> <string name="to_main_menu">Close</string>
@ -13,4 +13,13 @@
<string name="close_button">Close</string> <string name="close_button">Close</string>
<string name="logout_text">Logout</string> <string name="logout_text">Logout</string>
<string name="last_visit">"Last visit:"</string> <string name="last_visit">"Last visit:"</string>
<string name="password_input_hint">Password</string>
<string name="block_card_button_text">Block card</string>
<string name="card_using_header">Pass using</string>
<string name="find_employee_button_text">Find employee</string>
<string name="find_button_text">Find</string>
<string name="offline_error">Seems you are offline...</string>
<string name="no_internet_instructions">Check your internet connection\\nand try again</string>
<string name="reload_text">" Reload"</string>
<string name="unblock_card_button_text">Unblock card</string>
</resources> </resources>

View File

@ -1,8 +1,8 @@
<resources> <resources>
<string name="app_name">NTO Pass</string> <string name="app_name">QR Pass</string>
<string name="scan_qr_text">Сканировать QR-код</string> <string name="scan_qr_text">Сканировать QR-код</string>
<string name="login_hint">Введите логин</string> <string name="login_hint">Введите логин</string>
<string name="welcome_text">Добро пожаловать в NTO Pass</string> <string name="welcome_text">Добро пожаловать в\nQR Pass</string>
<string name="login">Войти</string> <string name="login">Войти</string>
<string name="refresh_data">Обновить данные</string> <string name="refresh_data">Обновить данные</string>
<string name="to_main_menu">Закрыть</string> <string name="to_main_menu">Закрыть</string>
@ -12,10 +12,12 @@
<string name="logout_text">Выйти из аккаунта</string> <string name="logout_text">Выйти из аккаунта</string>
<string name="last_visit">Последний вход: </string> <string name="last_visit">Последний вход: </string>
<string name="password_input_hint">Введите пароль</string> <string name="password_input_hint">Введите пароль</string>
<string name="register_process_button_text">Зарегестрироваться</string>
<string name="block_card_button_text">Заблокировать карту</string> <string name="block_card_button_text">Заблокировать карту</string>
<string name="card_using_header">Использование пропуска</string> <string name="card_using_header">Использование пропуска</string>
<string name="find_employee_button_text">Поиск сотрудника</string> <string name="find_employee_button_text">Поиск сотрудника</string>
<string name="find_button_text">Найти</string> <string name="find_button_text">Найти</string>
<string name="find_employee_header">Поиск сотрудника</string> <string name="offline_error">Кажется вы оффлайн...</string>
<string name="no_internet_instructions">Проверьте подключение к сети\nи попробуйте еще раз</string>
<string name="reload_text">Перезагрузить</string>
<string name="unblock_card_button_text">Разблокировать карту</string>
</resources> </resources>

View File

@ -0,0 +1,49 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.AdminRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.AdminNetworkDataSource
import ru.myitschool.work.domain.admin.AdminRepository
@RunWith(RobolectricTestRunner::class)
class AdminRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: AdminRepository
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = AdminRepositoryImpl(
networkDataSource = AdminNetworkDataSource,
localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When user have admin permissions and blocked user exist process` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertNull(repository.blockUser("").exceptionOrNull())
}
@Test
fun `When user don not have admin permissions error` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNotNull(repository.blockUser("").exceptionOrNull())
}
}

View File

@ -0,0 +1,68 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.PassRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.PassNetworkDataSource
import ru.myitschool.work.domain.entities.PassEntity
@RunWith(RobolectricTestRunner::class)
class PassesRepositoryImplTest {
private lateinit var repository: PassRepositoryImpl
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = PassRepositoryImpl(
networkDataSource = PassNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When user authorized as user gets only its passes`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertEquals(
listOf(
PassEntity("", "", ""),
PassEntity("", "", ""),
PassEntity("", "", "")
),
repository.getCurrentPasses(1, 3).getOrNull()
)
}
@Test
fun `When user authorized as admin gets any user's passes`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertEquals(
listOf(
PassEntity("", "", ""),
PassEntity("", "", ""),
PassEntity("", "", "")
),
repository.getUsersPasses(1, 3, "").getOrNull()
)
}
@Test
fun `When user tries to access admin method error` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.getUsersPasses(1, 3, ""))
}
}

View File

@ -0,0 +1,55 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
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
@RunWith(RobolectricTestRunner::class)
class QrRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: QrRepositoryImpl
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.SERVER_ADDRESS,
Context.MODE_PRIVATE
)
)
repository = QrRepositoryImpl(
networkDataSource = QrNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When QR valid and user logged process`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.pushQr(QrEntity("")).exceptionOrNull())
}
@Test
fun `When QR invalid and user logged error`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNotNull(repository.pushQr(QrEntity("*")).exceptionOrNull())
}
@Test
fun `When QR valid and user not logged error`() = runTest {
runCatching {
repository.pushQr(QrEntity("*")).exceptionOrNull()
}.let { assert(it.isFailure) }
}
}

View File

@ -0,0 +1,130 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
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.UserNetworkDataSource
import ru.myitschool.work.domain.entities.UserEntity
@RunWith(RobolectricTestRunner::class)
class UserRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: UserRepositoryImpl
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = UserRepositoryImpl(
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(),
userLocalDataSource = UserLocalDataSource,
networkDataSource = UserNetworkDataSource
)
}
@Test
fun `When user exist return true`() = runTest {
assertEquals(true, repository.isUserExist("pivanov").getOrNull())
}
@Test
fun `When user does not exist return false`() = runTest {
assertEquals(false, repository.isUserExist("pivanov").getOrNull())
}
@Test
fun `When user existing and password matches success login`() = runTest {
assertEquals(
UserEntity(
isAdmin = true,
name = "",
lastVisit = "",
photoUrl = "",
position = "",
isCardBlocked = false
), repository.login("pivanov", "admin").getOrNull()
)
}
@Test
fun `When user enter incorrect login`() = runTest {
assertNull(repository.login("***", "admin").getOrNull())
}
@Test
fun `When user enter incorrect password`() = runTest {
assertNull(repository.login("pivanov", "***").getOrNull())
}
@Test
fun `When user enter incorrect login and password`() = runTest {
assertNull(repository.login("***", "***").getOrNull())
}
@Test
fun `When user logouts local credentials wipes`() = runTest {
repository.logout()
runCatching {
CredentialsLocalDataSource.getInstance().getToken()
}.let { assert(it.isFailure) }
}
@Test
fun `Get user by login returns correct user`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertEquals(
UserEntity(
isAdmin = false,
name = "",
lastVisit = "",
photoUrl = "",
position = "",
isCardBlocked = false
),
repository.getUserByLogin("").getOrNull()
)
}
@Test
fun `Get user by login works with admin roots`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertNotNull(repository.getUserByLogin("").getOrNull())
}
@Test
fun `Get user by login don not works without admin roots`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.getUserByLogin("").getOrNull())
}
@Test
fun `When user successfully logged for the first time it will authorize automatically`() =
runTest {
val loggedUser = repository.login("pivanov", "admin").getOrNull()
assertEquals(
loggedUser,
repository.authorize(CredentialsLocalDataSource.getInstance().getToken())
)
}
@Test
fun `When user successfully logged its info saved locally for current session`() = runTest {
val loggedUser = repository.login("pivanov", "admin").getOrNull()
assertEquals(loggedUser, repository.getCurrentUser())
}
}