fixes / feat: some bugs with dto fixed and block / unblock use case done

This commit is contained in:
a1pha 2025-02-20 15:30:21 +03:00
parent f81befe9bd
commit 3aa012adf9
26 changed files with 191 additions and 95 deletions

View File

@ -7,8 +7,12 @@ import ru.myitschool.work.domain.admin.AdminRepository
class AdminRepositoryImpl(
private val networkDataSource: AdminNetworkDataSource,
private val localCredentialsLocalDataSource: CredentialsLocalDataSource
): AdminRepository {
) : 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

@ -40,8 +40,8 @@ class PassRepositoryImpl(
return listDto.map { successListDto ->
successListDto.mapNotNull { dto ->
PassEntity(
type = dto.type ?: return@mapNotNull null,
name = dto.name ?: return@mapNotNull null,
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
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.QrNetworkDataSource
import ru.myitschool.work.domain.entities.QrEntity
import ru.myitschool.work.domain.qr.QrRepository

View File

@ -21,7 +21,9 @@ class UserRepositoryImpl(
.fold(
onSuccess = { dto ->
map(dto).fold(
onSuccess = { userLocalDataSource.cacheData(it) },
onSuccess = {
userLocalDataSource.cacheData(it)
},
onFailure = { error(it) }
)
},
@ -32,8 +34,13 @@ class UserRepositoryImpl(
override suspend fun authorize(token: String): Result<Unit> {
return networkDataSource.login(token).fold(
onSuccess = { Result.success(Unit) },
onFailure = { error -> Result.failure(error) }
onSuccess = { dto ->
map(dto).fold(
onSuccess = { Result.success(userLocalDataSource.cacheData(it)) },
onFailure = { Result.failure(it) }
)
},
onFailure = { Result.failure(it) }
)
}
@ -54,7 +61,7 @@ class UserRepositoryImpl(
return runCatching {
UserEntity(
name = userDto.name ?: error("Null user name"),
lastVisit = userDto.lastVisit ?: error("Null user lastVisit"),
lastVisit = userDto.lastVisit ?: "",
photoUrl = userDto.photoUrl ?: error("Null user photoUrl"),
position = userDto.position ?: error("Null user position"),
isAdmin = userDto.roleId?.let { it ->
@ -62,7 +69,8 @@ class UserRepositoryImpl(
it,
credentialsLocalDataSource.getToken()
).fold(onSuccess = { it }, onFailure = { error(it) })
} ?: error("Null user roleId")
} ?: error("Null user roleId"),
isCardBlocked = userDto.isCardBlocked ?: error("Null user isCardBlocked")
)
}
}

View File

@ -6,10 +6,16 @@ import kotlinx.serialization.Serializable
@Serializable
data class PassDto(
@SerialName("time")
val time: String?,
@SerialName("localDateTime")
val terminal: TerminalDto?
)
@Serializable
data class TerminalDto(
@SerialName("type")
val type: String?,
@SerialName("name")
val name: String?,
@SerialName("time")
val time: String?
val name: String?
)

View File

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class UserDto(
@SerialName("roleId")
@SerialName("authority_id")
val roleId: Int?,
@SerialName("name")
val name: String?,
@ -15,7 +15,7 @@ data class UserDto(
val photoUrl: String?,
@SerialName("position")
val position: String?,
@SerialName("login")
@SerialName("username")
val login: String?,
@SerialName("isCardBlocked")
val isCardBlocked: Boolean?

View File

@ -15,7 +15,7 @@ object AdminNetworkDataSource {
suspend fun blockUser(login: String, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response = client.patch("$SERVER_ADDRESS/api/employees/block&login=${login}") {
val response = client.patch("$SERVER_ADDRESS/api/employees/block?login=$login") {
headers {
append(HttpHeaders.Authorization, token)
}
@ -27,4 +27,18 @@ object AdminNetworkDataSource {
}
}
suspend fun unblockUser(login: String, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response = client.patch("$SERVER_ADDRESS/api/employees/unblock?login=$login") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
}

View File

@ -21,7 +21,7 @@ object PassNetworkDataSource {
withContext(Dispatchers.IO) {
runCatching {
val response =
client.get("$SERVER_ADDRESS/api/passes?pageNum=$pageNum&pageSize=$pageSize") {
client.get("$SERVER_ADDRESS/api/passes/paginated/?page=$pageNum&size=$pageSize") {
headers {
append(HttpHeaders.Authorization, token)
}

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.patch
@ -11,28 +11,25 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.QrDto
import ru.myitschool.work.data.network.KtorClient
import ru.myitschool.work.domain.entities.QrEntity
object QrNetworkDataSource {
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val response = KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/push_qr") {
headers {
append(HttpHeaders.Authorization, token)
}
contentType(ContentType.Application.Json)
setBody(
QrDto(code = qrEntity.code)
)
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response =
KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/open") {
headers {
append(HttpHeaders.Authorization, token)
}
contentType(ContentType.Application.Json)
setBody(QrDto(qrEntity.code))
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
}

View File

@ -14,7 +14,7 @@ object UserNetworkDataSource {
suspend fun isUserExist(login: String): Result<Boolean> = withContext(Dispatchers.IO) {
runCatching {
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/employees/$login")
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/username/$login")
result.status == HttpStatusCode.OK
}
}
@ -23,7 +23,7 @@ object UserNetworkDataSource {
withContext(Dispatchers.IO) {
runCatching {
val result =
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/employees/login") {
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") {
headers {
append(HttpHeaders.Authorization, token)
}
@ -38,7 +38,7 @@ object UserNetworkDataSource {
withContext(Dispatchers.IO) {
runCatching {
val response =
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/roles/info?$roleId") {
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/authority/$roleId") {
headers {
append(HttpHeaders.Authorization, token)
}

View File

@ -2,4 +2,5 @@ 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 UnblockUserUseCase(
private val repository: AdminRepository
) {
suspend operator fun invoke(login: String) = repository.unblockUser(login)
}

View File

@ -6,5 +6,5 @@ data class UserEntity(
val lastVisit: String,
val photoUrl: String,
val position: String,
val isCardBlocked: Boolean
)

View File

@ -21,6 +21,7 @@ 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
@ -30,6 +31,7 @@ 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() {
@ -88,6 +90,12 @@ class AdminViewModel(
}
}
fun unblock() {
viewModelScope.launch {
unBlockUserUseCase(currentLogin!!)
}
}
sealed interface State {
data class Show(val user: UserEntity) : State
data object Waiting : State
@ -100,6 +108,10 @@ class AdminViewModel(
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(
@ -115,10 +127,10 @@ class AdminViewModel(
)
),
blockUserUseCase = BlockUserUseCase(
repository = AdminRepositoryImpl(
networkDataSource = AdminNetworkDataSource,
localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
repository = adminRepository
),
unBlockUserUseCase = UnblockUserUseCase(
repository = adminRepository
)
) as T
}

View File

@ -26,20 +26,20 @@ class ViewUserAsAdminFragment : Fragment(R.layout.fragment_user) {
binding.findUser.visibleOrGone(false)
binding.logout.visibleOrGone(false)
binding.block.visibleOrGone(true)
val adapter = PassesListAdapter()
binding.passes.adapter = adapter
viewModel.state.collectWithLifecycle(this) { state ->
binding.refresh.isRefreshing = state is AdminViewModel.State.Loading
binding.content?.visibleOrGone(state is AdminViewModel.State.Show)
binding.content.visibleOrGone(state is AdminViewModel.State.Show)
when (state) {
is AdminViewModel.State.Loading -> Unit
is AdminViewModel.State.Show -> {
val user = state.user
binding.findUser.visibleOrGone(user.isAdmin)
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
@ -79,6 +79,10 @@ class ViewUserAsAdminFragment : Fragment(R.layout.fragment_user) {
.show()
}
binding.unblock.setOnClickListener {
viewModel.unblock()
}
}
override fun onDestroy() {

View File

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

View File

@ -15,7 +15,7 @@ class PassesPagingSource(
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PassEntity> {
val pageNum = params.key ?: 0
val pageNum = params.key ?: 1
return request.invoke(pageNum, params.loadSize).fold(
onSuccess = { value ->
LoadResult.Page(

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.ui.profile
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
@ -10,9 +11,12 @@ 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.qr.result.RESPONSE_KEY
import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.utils.collectWithLifecycle
import ru.myitschool.work.utils.visibleOrGone
class UserFragment : Fragment(R.layout.fragment_user) {
private var _binding: FragmentUserBinding? = null
@ -28,11 +32,13 @@ class UserFragment : Fragment(R.layout.fragment_user) {
viewModel.state.collectWithLifecycle(this) { state ->
binding.refresh.isRefreshing = state is UserViewModel.State.Loading
binding.content?.visibleOrGone(state is UserViewModel.State.Show)
binding.content.visibleOrGone(state is UserViewModel.State.Show)
Log.d("info", state.toString())
when (state) {
is UserViewModel.State.Loading -> Unit
is UserViewModel.State.Show -> {
val user = state.userEntity
binding.scan.visibleOrGone(!user.isCardBlocked)
binding.findUser.visibleOrGone(user.isAdmin)
binding.fullname.text = user.name
binding.position.text = user.position
@ -65,14 +71,27 @@ class UserFragment : Fragment(R.layout.fragment_user) {
.setTitle("Выход")
.setMessage("Вы уверены, что хотите выйти из аккаунта?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.onLogout() }
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.onLogout()
findNavController().navigate(R.id.action_userFragment_to_loginFragment)
}
.show()
findNavController().navigate(R.id.action_userFragment_to_loginFragment)
}
binding.findUser.setOnClickListener {
findNavController().navigate(R.id.find_user)
}
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)
}
}
}

View File

@ -47,7 +47,8 @@ class UserViewModel(
private fun updateState() {
viewModelScope.launch {
State.Show(getCurrentUserUseCase())
_state.emit(State.Loading)
_state.emit(State.Show(getCurrentUserUseCase()))
}
}

View File

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

View File

@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.QrNetworkDataSource
import ru.myitschool.work.data.network.QrNetworkDataSource
import ru.myitschool.work.data.QrRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.domain.entities.QrEntity
@ -20,9 +20,16 @@ class QrResultViewModel(
private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow()
fun update(qrValue: String) {
var _qrEntity: QrEntity? = null
private val qrEntity: QrEntity get() = _qrEntity!!
fun setQr(qrEntity: QrEntity) {
_qrEntity = qrEntity
}
fun update() {
viewModelScope.launch {
pushQrUseCase(QrEntity(code = qrValue)).fold(
pushQrUseCase(qrEntity).fold(
onSuccess = { _state.emit(State.Show) },
onFailure = { _state.emit(State.Error(it.message.toString())) }
)

View File

@ -14,6 +14,7 @@
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="20dp"
@ -132,7 +133,21 @@
android:background="@drawable/warn_button_outline"
android:foreground="?attr/selectableItemBackground"
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
android:id="@+id/logout"

View File

@ -1,9 +1,21 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="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
android:id="@+id/refresh"
android:layout_width="match_parent"
@ -95,15 +107,6 @@
android:foreground="?attr/selectableItemBackground"
android:text="@string/scan_qr_text"
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
@ -123,8 +126,20 @@
android:text="@string/block_card_button_text"
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
android:id="@+id/logout"
android:layout_width="wrap_content"

View File

@ -65,6 +65,11 @@
<action
android:id="@+id/action_splashFragment_to_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
android:id="@+id/adminFragment"

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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="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="refresh_data">Refresh</string>
<string name="to_main_menu">Close</string>
@ -21,4 +21,5 @@
<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>

View File

@ -1,8 +1,8 @@
<resources>
<string name="app_name">NTO Pass</string>
<string name="app_name">QR Pass</string>
<string name="scan_qr_text">Сканировать QR-код</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="refresh_data">Обновить данные</string>
<string name="to_main_menu">Закрыть</string>
@ -19,4 +19,5 @@
<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>