From 9393cb89b026a8f35723093529fcdea13fe32978 Mon Sep 17 00:00:00 2001 From: a1pha Date: Wed, 19 Feb 2025 18:53:46 +0300 Subject: [PATCH] feat: admin section done --- .../work/data/AdminRepositoryImpl.kt | 14 ++ .../data/network/AdminNetworkDataSource.kt | 30 +++++ .../work/domain/admin/AdminRepository.kt | 5 + .../work/domain/admin/BlockUserUseCase.kt | 8 ++ .../domain/passes/GetUsersPassesUseCase.kt | 9 ++ .../ru/myitschool/work/ui/RootActivity.kt | 7 +- .../work/ui/admin/search/AdminFragment.kt | 41 ++++++ .../work/ui/admin/search/AdminViewModel.kt | 127 ++++++++++++++++++ .../ui/admin/view/UsersPassesPagingSource.kt | 31 +++++ .../ui/admin/view/ViewUserAsAdminFragment.kt | 88 ++++++++++++ 10 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/ru/myitschool/work/data/AdminRepositoryImpl.kt create mode 100644 app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt create mode 100644 app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt diff --git a/app/src/main/java/ru/myitschool/work/data/AdminRepositoryImpl.kt b/app/src/main/java/ru/myitschool/work/data/AdminRepositoryImpl.kt new file mode 100644 index 0000000..3cd5a62 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/AdminRepositoryImpl.kt @@ -0,0 +1,14 @@ +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 { + return networkDataSource.blockUser(login, localCredentialsLocalDataSource.getToken()) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt b/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt new file mode 100644 index 0000000..332e6ee --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/data/network/AdminNetworkDataSource.kt @@ -0,0 +1,30 @@ +package ru.myitschool.work.data.network + +import io.ktor.client.request.headers +import io.ktor.client.request.patch +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.myitschool.work.core.Constants.SERVER_ADDRESS + +object AdminNetworkDataSource { + + private val client = KtorClient.client + + suspend fun blockUser(login: String, token: String): Result = + withContext(Dispatchers.IO) { + runCatching { + val response = client.patch("$SERVER_ADDRESS/api/employees/block&login=${login}") { + headers { + append(HttpHeaders.Authorization, token) + } + } + + if (response.status != HttpStatusCode.OK) + error("Status ${response.status}") + Unit + } + } + +} diff --git a/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt b/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt new file mode 100644 index 0000000..d4730ff --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/admin/AdminRepository.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.domain.admin + +interface AdminRepository { + suspend fun blockUser(login: String): Result +} diff --git a/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt new file mode 100644 index 0000000..1a90483 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/admin/BlockUserUseCase.kt @@ -0,0 +1,8 @@ +package ru.myitschool.work.domain.admin + +class BlockUserUseCase( + private val repository: AdminRepository +) { + + suspend operator fun invoke(login: String) = repository.blockUser(login) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt b/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt new file mode 100644 index 0000000..c41854f --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/domain/passes/GetUsersPassesUseCase.kt @@ -0,0 +1,9 @@ +package ru.myitschool.work.domain.passes + +class GetUsersPassesUseCase( + private val repository: PassRepository +) { + + suspend operator fun invoke(pageNum: Int, pageSize: Int, login: String) = + repository.getUsersPasses(pageNum, pageSize, login) +} diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt index 3ff6bc2..653854d 100644 --- a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -4,13 +4,18 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint import ru.myitschool.work.R +import ru.myitschool.work.utils.isOnline -// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! @AndroidEntryPoint class RootActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_root) + if (!isOnline(this)) { + val dialog = NoInternetNotificationFragment() + dialog.isCancelable = false + dialog.show(supportFragmentManager, "NO_INTERNET") + } } } \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt new file mode 100644 index 0000000..068f10d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminFragment.kt @@ -0,0 +1,41 @@ +package ru.myitschool.work.ui.admin.search + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentFindEmployeeBinding +import ru.myitschool.work.utils.collectWithLifecycle + +class AdminFragment : Fragment(R.layout.fragment_find_employee) { + + private var _binding: FragmentFindEmployeeBinding? = null + private val binding: FragmentFindEmployeeBinding get() = _binding!! + + private val viewModel by viewModels { AdminViewModel.Factory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentFindEmployeeBinding.bind(view) + + viewModel.state.collectWithLifecycle(this) { state -> + binding.username.isEnabled = state !is AdminViewModel.State.Loading + when (state) { + is AdminViewModel.State.Error -> binding.error.text = state.errorMessage + is AdminViewModel.State.Loading -> Unit + is AdminViewModel.State.Show -> {} + is AdminViewModel.State.Waiting -> Unit + } + } + + binding.find.setOnClickListener { + viewModel.onFind(binding.username.text.toString()) + } + + } + + override fun onDestroy() { + _binding = null + super.onDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt new file mode 100644 index 0000000..a8427f0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/search/AdminViewModel.kt @@ -0,0 +1,127 @@ +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.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 blockUserUseCase: BlockUserUseCase +) : ViewModel() { + + private val _state = MutableStateFlow(State.Waiting) + val state = _state.asStateFlow() + + private var _listState: Flow>? = null + val listState: Flow> get() = _listState!! + + private var currentLogin: String? = null + + 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!!) + } + } + + sealed interface State { + data class Show(val user: UserEntity) : State + data object Waiting : State + data object Loading : State + data class Error(val errorMessage: String) : State + } + + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class, extras: CreationExtras): T { + 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 = AdminRepositoryImpl( + networkDataSource = AdminNetworkDataSource, + localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance() + ) + ) + ) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt b/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt new file mode 100644 index 0000000..34789b9 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/view/UsersPassesPagingSource.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.ui.admin.view + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import ru.myitschool.work.domain.entities.PassEntity + +class UsersPassesPagingSource( + private val request: suspend (pageNum: Int, pageSize: Int, login: String) -> Result>, + private val login: String +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) + ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val pageNum = params.key ?: 0 + return request.invoke(pageNum, params.loadSize, login).fold( + onSuccess = { value -> + LoadResult.Page( + data = value, + prevKey = (pageNum - 1).takeIf { it >= 0 }, + nextKey = (pageNum + 1).takeIf { value.size == params.loadSize } + ) + }, + onFailure = { error -> LoadResult.Error(error) } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt b/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt new file mode 100644 index 0000000..10cc6ab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/admin/view/ViewUserAsAdminFragment.kt @@ -0,0 +1,88 @@ +package ru.myitschool.work.ui.admin.view + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.paging.LoadState +import com.squareup.picasso.Picasso +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentUserBinding +import ru.myitschool.work.ui.admin.search.AdminViewModel +import ru.myitschool.work.ui.profile.PassesListAdapter +import ru.myitschool.work.utils.collectWithLifecycle +import ru.myitschool.work.utils.visibleOrGone + +class ViewUserAsAdminFragment : Fragment(R.layout.fragment_user) { + + private var _binding: FragmentUserBinding? = null + private val binding: FragmentUserBinding get() = _binding!! + + private val viewModel by viewModels { AdminViewModel.Factory } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentUserBinding.bind(view) + + binding.findUser.visibleOrGone(false) + binding.logout.visibleOrGone(false) + 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) + + when (state) { + is AdminViewModel.State.Loading -> Unit + is AdminViewModel.State.Show -> { + val user = state.user + binding.findUser.visibleOrGone(user.isAdmin) + binding.fullname.text = user.name + binding.position.text = user.position + binding.lastEntry.text = user.lastVisit + Picasso.get().load(user.photoUrl).into(binding.photo) + } + + 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() + } + + } + + override fun onDestroy() { + _binding = null + super.onDestroy() + } +} \ No newline at end of file