feat: Получение данных пользователя по логину + фиксы багов

This commit is contained in:
yastruckov 2025-02-20 12:32:31 +03:00
parent 0446bddcc9
commit 041db2955f
13 changed files with 221 additions and 80 deletions

View File

@ -1,59 +0,0 @@
package ru.myitschool.work
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [admin.newInstance] factory method to
* create an instance of this fragment.
*/
class admin : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_admin, container, false)
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment admin.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
admin().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}

View File

@ -28,7 +28,7 @@ class LastEntranceNetworkDataSource(
basicAuth(username, password) basicAuth(username, password)
} }
} }
if (result.status != HttpStatusCode.OK) { if (result.status != HttpStatusCode.OK && result.status != HttpStatusCode.NoContent) {
error("Status ${result.status}") error("Status ${result.status}")
} }
result.body() result.body()

View File

@ -3,6 +3,7 @@ package ru.myitschool.work.data.profile.admin
import android.content.Context import android.content.Context
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.basicAuth import io.ktor.client.request.basicAuth
import io.ktor.client.request.get
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@ -10,28 +11,43 @@ import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.myitschool.work.R
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.dto.EmployeeDTO import ru.myitschool.work.dto.EmployeeDTO
import ru.myitschool.work.utils.NetworkModule import ru.myitschool.work.utils.NetworkModule
class EmployeeNetworkDataSource( class EmployeeInfoNetworkDataSource(
context: Context private val context: Context
) { ) {
private val client = NetworkModule.httpClient private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context) private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun getProfile(login : String):Result<EmployeeDTO> = withContext(Dispatchers.IO){ suspend fun getProfile(login : String):Result<EmployeeDTO> = withContext(Dispatchers.IO){
runCatching { runCatching {
println(login)
val username = userDataStoreManager.usernameFlow.first() val username = userDataStoreManager.usernameFlow.first()
println(username)
val password = userDataStoreManager.passwordFlow.first() val password = userDataStoreManager.passwordFlow.first()
val result = client.post("${Constants.SERVER_ADDRESS}/api/employee/$login"){ val result = client.get("${Constants.SERVER_ADDRESS}/api/employee/$login"){
headers{ headers{
basicAuth(username, password) basicAuth(username, password)
} }
} }
if (result.status != HttpStatusCode.OK) { when(result.status){
error("Status ${result.status}") HttpStatusCode.Unauthorized -> {context.getString(R.string.admin_unauthorized)}
HttpStatusCode.Forbidden -> error(context.getString(R.string.admin_forbidden))
HttpStatusCode.NotFound -> error(context.getString(R.string.not_found))
HttpStatusCode.OK -> result.body()
} }
if(result.status == HttpStatusCode.Unauthorized){
error(context.getString(R.string.admin_unauthorized))
}
if (result.status != HttpStatusCode.OK) {
println(result.status)
error("Status ${result.status}")
}
println(result.bodyAsText()) println(result.bodyAsText())
result.body() result.body()
} }

View File

@ -1,11 +1,11 @@
package ru.myitschool.work.data.profile.admin package ru.myitschool.work.data.profile.admin
import ru.myitschool.work.entities.EmployeeEntity import ru.myitschool.work.entities.EmployeeEntity
import ru.myitschool.work.domain.profile.admin.EmployeeProfileRepo import ru.myitschool.work.domain.profile.admin.EmployeeInfoRepo
class EmployeeProfileRepoImpl( class EmployeeInfoRepoImpl(
private val networkDataSource: EmployeeNetworkDataSource private val networkDataSource: EmployeeInfoNetworkDataSource
) : EmployeeProfileRepo { ) : EmployeeInfoRepo {
override suspend fun getInfo(login : String): Result<EmployeeEntity> { override suspend fun getInfo(login : String): Result<EmployeeEntity> {
return networkDataSource.getProfile(login).map { dto -> return networkDataSource.getProfile(login).map { dto ->
EmployeeEntity( EmployeeEntity(

View File

@ -2,6 +2,6 @@ package ru.myitschool.work.domain.profile.admin
import ru.myitschool.work.entities.EmployeeEntity import ru.myitschool.work.entities.EmployeeEntity
interface EmployeeProfileRepo { interface EmployeeInfoRepo {
suspend fun getInfo(login : String): Result<EmployeeEntity> suspend fun getInfo(login : String): Result<EmployeeEntity>
} }

View File

@ -1,7 +1,7 @@
package ru.myitschool.work.domain.profile.admin package ru.myitschool.work.domain.profile.admin
class GetEmployeeProfileUseCase( class GetEmployeeInfoUseCase(
private val repo : EmployeeProfileRepo private val repo : EmployeeInfoRepo
) { ) {
suspend operator fun invoke(login : String) = repo.getInfo(login) suspend operator fun invoke(login : String) = repo.getInfo(login)
} }

View File

@ -0,0 +1,83 @@
package ru.myitschool.work.ui.admin
import android.content.res.ColorStateList
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentAdminBinding
import ru.myitschool.work.entities.EmployeeEntity
import ru.myitschool.work.ui.login.LoginViewModel
import ru.myitschool.work.utils.collectWithLifecycle
class AdminFragment : Fragment(R.layout.fragment_admin) {
private var _binding: FragmentAdminBinding? = null
private val binding get() = _binding!!
private val viewModel: AdminViewModel by viewModels{ AdminViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentAdminBinding.bind(view)
binding.searchBtn.setOnClickListener {
viewModel.searchUser(
binding.search.text.toString()
)
}
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun afterTextChanged(s: Editable?) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val search = binding.search.text
val isEnabled =
search.length >= 3 && !search[0].isDigit() && search.matches(Regex("^[a-zA-Z0-9]*$"))
binding.searchBtn.isEnabled = isEnabled
if (isEnabled) {
binding.searchBtn.setBackgroundColor(resources.getColor(R.color.accent_color))
binding.searchBtn.imageTintList = ColorStateList.valueOf(
ContextCompat.getColor(
requireContext(),
R.color.white
))
} else {
binding.searchBtn.setBackgroundColor(resources.getColor(R.color.bg_color))
binding.searchBtn.imageTintList = ColorStateList.valueOf(
ContextCompat.getColor(
requireContext(),
R.color.secondary_text_color
))
}
}
}
binding.search.addTextChangedListener(textWatcher)
viewModel.infoState.collectWithLifecycle(this){ state ->
when(state){
is AdminViewModel.SearchState.Error -> {
binding.error.visibility = View.VISIBLE
binding.error.text = state.message
}
AdminViewModel.SearchState.Loading -> {
binding.error.visibility = View.GONE
binding
}
is AdminViewModel.SearchState.Success -> {
binding.error.visibility = View.GONE
showUserData(state.data)
}
}
}
}
private fun showUserData(user: EmployeeEntity){
binding.userName.text = user.name
binding.position.text = user.position
}
}

View File

@ -0,0 +1,61 @@
package ru.myitschool.work.ui.admin
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.profile.admin.EmployeeInfoNetworkDataSource
import ru.myitschool.work.data.profile.admin.EmployeeInfoRepoImpl
import ru.myitschool.work.domain.profile.admin.EmployeeInfoRepo
import ru.myitschool.work.domain.profile.admin.GetEmployeeInfoUseCase
import ru.myitschool.work.entities.EmployeeEntity
class AdminViewModel(
private val getInfoUseCase: GetEmployeeInfoUseCase,
application: Application
) : AndroidViewModel(application) {
private val _infoState = MutableStateFlow<SearchState>(SearchState.Loading)
val infoState: StateFlow<SearchState> = _infoState.asStateFlow()
sealed class SearchState {
data object Loading : SearchState()
data class Success(val data: EmployeeEntity) : SearchState()
data class Error(val message: String?) : SearchState()
}
fun searchUser(login : String){
_infoState.value = SearchState.Loading
viewModelScope.launch {
getInfoUseCase.invoke(login).fold(
onSuccess = { data ->
_infoState.value = SearchState.Success(data)
},
onFailure = { e ->
_infoState.value = SearchState.Error(e.message)
}
)
}
}
companion object {
@Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val infoRepoImpl = EmployeeInfoRepoImpl(
networkDataSource = EmployeeInfoNetworkDataSource(
context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
)
)
val useCase = GetEmployeeInfoUseCase(infoRepoImpl)
return AdminViewModel(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T
}
}
}
}

View File

@ -23,6 +23,7 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentLoginBinding.bind(view) _binding = FragmentLoginBinding.bind(view)
val textWatcher = object : TextWatcher{ val textWatcher = object : TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
@ -37,9 +38,11 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
binding.loginBtn.isEnabled = isEnabled binding.loginBtn.isEnabled = isEnabled
if(isEnabled){ if(isEnabled){
binding.loginBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.accent_color)) binding.loginBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.accent_color))
binding.loginBtn.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
} }
else{ else{
binding.loginBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.bg_color)) binding.loginBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.bg_color))
binding.loginBtn.setTextColor(ContextCompat.getColor(requireContext(), R.color.secondary_text_color))
} }
} }

View File

@ -1,7 +1,10 @@
package ru.myitschool.work.ui.main package ru.myitschool.work.ui.main
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
@ -9,6 +12,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.map
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -36,7 +40,7 @@ class MainFragment : Fragment(R.layout.fragment_main) {
viewModel.getUserData() viewModel.getUserData()
viewModel.getLastEntryDate() viewModel.getLastEntryDate()
findNavController().navigate(R.id.admin)
binding.logout.setOnClickListener { logout() } binding.logout.setOnClickListener { logout() }
binding.scan.setOnClickListener { onScanClick() } binding.scan.setOnClickListener { onScanClick() }
viewModel.userState.collectWhenStarted(this) { state -> viewModel.userState.collectWhenStarted(this) { state ->
@ -125,15 +129,27 @@ class MainFragment : Fragment(R.layout.fragment_main) {
} }
private fun showUserData(employeeEntity: EmployeeEntity) { private fun showUserData(employeeEntity: EmployeeEntity) {
binding.apply {
fullname.text = employeeEntity.name
println(employeeEntity.name)
position.text = employeeEntity.position
Picasso.get().load(employeeEntity.photoUrl).into(photo)
error.visibility = View.GONE binding.fullname.text = employeeEntity.name
println(employeeEntity.name)
binding.position.text = employeeEntity.position
Picasso.get().load(employeeEntity.photoUrl).into(binding.photo)
binding.error.visibility = View.GONE
setViewsVisibility(View.VISIBLE) setViewsVisibility(View.VISIBLE)
binding.scan.isEnabled = employeeEntity.qrEnabled
when(employeeEntity.qrEnabled){
true -> {
binding.scan.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.accent_color))
binding.scan.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
} }
false -> {
binding.scan.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.bg_color))
binding.scan.setTextColor(ContextCompat.getColor(requireContext(), R.color.secondary_text_color))
}
}
} }
private fun showError() { private fun showError() {
@ -147,6 +163,8 @@ class MainFragment : Fragment(R.layout.fragment_main) {
binding.photo.visibility = visibility binding.photo.visibility = visibility
binding.logout.visibility = visibility binding.logout.visibility = visibility
binding.scan.visibility = visibility binding.scan.visibility = visibility
binding.blockMain.visibility = visibility
binding.blockHistory.visibility = visibility
} }

View File

@ -12,6 +12,9 @@
<action <action
android:id="@+id/action_loginFragment_to_mainFragment" android:id="@+id/action_loginFragment_to_mainFragment"
app:destination="@id/mainFragment" /> app:destination="@id/mainFragment" />
<action
android:id="@+id/action_loginFragment_to_admin"
app:destination="@id/admin" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/mainFragment" android:id="@+id/mainFragment"
@ -21,6 +24,9 @@
<action <action
android:id="@+id/action_mainFragment_to_qrScanFragment" android:id="@+id/action_mainFragment_to_qrScanFragment"
app:destination="@id/qrScanFragment" /> app:destination="@id/qrScanFragment" />
<action
android:id="@+id/action_mainFragment_to_admin"
app:destination="@id/admin" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/qrScanFragment" android:id="@+id/qrScanFragment"
@ -40,4 +46,9 @@
android:id="@+id/action_qrResultFragment_to_mainFragment" android:id="@+id/action_qrResultFragment_to_mainFragment"
app:destination="@id/mainFragment" /> app:destination="@id/mainFragment" />
</fragment> </fragment>
<fragment
android:id="@+id/admin"
android:name="ru.myitschool.work.ui.admin.AdminFragment"
android:label="fragment_admin"
tools:layout="@layout/fragment_admin" />
</navigation> </navigation>

View File

@ -18,4 +18,8 @@
<string name="history_title">Visit history</string> <string name="history_title">Visit history</string>
<string name="login_unauthorized">Incorrect login or password</string> <string name="login_unauthorized">Incorrect login or password</string>
<string name="search_hint">Enter the employee\'s username</string> <string name="search_hint">Enter the employee\'s username</string>
<string name="admin_unauthorized">Unauthorized</string>
<string name="admin_forbidden">Forbidden \n
How did you get here?</string>
<string name="not_found"> 404 Not Found</string>
</resources> </resources>

View File

@ -20,6 +20,10 @@
<string name="history_title">История посещений</string> <string name="history_title">История посещений</string>
<string name="login_unauthorized">Неправильное имя пользователя или пароль</string> <string name="login_unauthorized">Неправильное имя пользователя или пароль</string>
<string name="search_hint">Введите логин сотрудника</string> <string name="search_hint">Введите логин сотрудника</string>
<string name="admin_unauthorized">Вы не авторизованы</string>
<string name="admin_forbidden">Не достаточно прав \n
Как ты сюда попал?</string>
<string name="not_found">404 Не найдено</string>
<!-- TODO: Remove or change this placeholder text --> <!-- TODO: Remove or change this placeholder text -->