add profile fragment

This commit is contained in:
Ксении 2025-02-19 16:29:22 +03:00
parent 9ba4ba21db
commit 45a5f9cff5
23 changed files with 424 additions and 89 deletions

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

@ -1,5 +1,6 @@
package ru.myitschool.work.data.auth
import android.util.Log
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
@ -17,22 +18,24 @@ import ru.myitschool.work.data.user.UserDto
object AuthNetworkDataSource {
suspend fun isUserExist(login: String): Result<Boolean?> = withContext(Dispatchers.IO) {
runCatching {
val result = client.get("$SERVER_ADDRESS/api/login/$login") //10.0.2.2
when (result.status) {
HttpStatusCode.OK -> { return@runCatching true }
HttpStatusCode.NotFound -> { return@runCatching false }
else -> {return@runCatching null }
}
}
}
// suspend fun isUserExist(login: String): Result<Boolean?> = withContext(Dispatchers.IO) {
// runCatching {
// val result = client.get("$SERVER_ADDRESS/api/login/$login") //10.0.2.2
// when (result.status) {
// HttpStatusCode.OK -> { return@runCatching true }
// HttpStatusCode.NotFound -> { return@runCatching false }
// else -> {return@runCatching null }
// }
// }
// }
suspend fun login(token: String): Result<UserDto> = withContext(Dispatchers.IO) {
runCatching {
val result = client.get("$SERVER_ADDRESS/api/login") {
header(HttpHeaders.Authorization, token)
}
Log.d("result", "${result.status}")
if (result.status == HttpStatusCode.Unauthorized) {
error("Неверный email или пароль")
}

@ -7,9 +7,9 @@ class AuthRepoImpl(
private val authNetworkDataSource: AuthNetworkDataSource,
private val authStorageDataSource: AuthStorageDataSource
) : AuthRepo {
override suspend fun isUserExist(email: String): Result<Boolean?> {
return authNetworkDataSource.isUserExist(email)
}
// override suspend fun isUserExist(email: String): Result<Boolean?> {
// return authNetworkDataSource.isUserExist(email)
// }
override suspend fun login(email: String, password: String): Result<UserDto> {
val token = authStorageDataSource.updateToken(email, password)

@ -0,0 +1,16 @@
package ru.myitschool.work.data.user
import kotlinx.serialization.SerialName
data class EntranceDto(
@SerialName("login")
val login : String,
@SerialName("name")
var name: String,
@SerialName("enterAt")
var enterAt: String,
@SerialName("enterType")
var enterType: String,
) {
}

@ -18,8 +18,8 @@ data class UserDto(
var avatarUrl: String?,
@SerialName("position")
val position: String,
@SerialName("lastEntry")
val lastEntry : String,
@SerialName("lastEnter")
val lastEntry : String? = null,
@SerialName("authorities")
val authorities : String
) {

@ -0,0 +1,52 @@
package ru.sicampus.bootcamp2025.data.user
import android.util.Log
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
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.auth.AuthStorageDataSource.token
import ru.myitschool.work.data.auth.Network.client
import ru.myitschool.work.data.user.EntranceDto
import ru.myitschool.work.data.user.UserDto
class UserNetworkDataSource {
suspend fun getUser(login : String): Result<UserDto> = withContext(Dispatchers.IO) {
runCatching {
val result = client.get("http://$SERVER_ADDRESS/api/${login}/info") {
header(HttpHeaders.Authorization, token)
}
Log.d("tututuut", "${result.status}")
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
Log.d("result", result.bodyAsText())
result.body()
}
}
suspend fun getEntrancesList(login : String) : Result<List<EntranceDto>> = withContext(Dispatchers.IO){
runCatching {
val result = client.get("http://$SERVER_ADDRESS/api/$login/entrances") {
header(HttpHeaders.Authorization, token)
}
Log.d("serverCode", "${result.status}")
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
result.body()
}
}
}

@ -0,0 +1,42 @@
package ru.sicampus.bootcamp2025.data.user
import ru.myitschool.work.data.user.EntranceDto
import ru.myitschool.work.data.user.UserDto
import ru.myitschool.work.domain.user.EntranceEntity
import ru.myitschool.work.domain.user.UserEntity
import ru.myitschool.work.domain.user.UserRepo
class UserRepoImpl (
private val userNetworkDataSource: UserNetworkDataSource
) : UserRepo {
override suspend fun getUser(login: String): Result<UserEntity> {
return userNetworkDataSource.getUser(login).map { dto ->
UserEntity(
id = dto.id ?: -1,
name = dto.name,
avatarUrl = dto.avatarUrl ?: "",
authorities = dto.authorities,
login = login,
position = dto.position,
lastEntry = dto.lastEntry ?: "",
)
}
}
override suspend fun getEntrancesList(login : String): Result<List<EntranceEntity>> {
return userNetworkDataSource.getEntrancesList(login).map { userList ->
userList.map { it.toEntity() }
}
}
fun EntranceDto.toEntity(): EntranceEntity {
return EntranceEntity(
login = login,
name = this.name,
enterAt = this.enterAt,
enterType = this.enterType
)
}
}

@ -3,6 +3,6 @@ package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.user.UserDto
interface AuthRepo {
suspend fun isUserExist(email: String): Result<Boolean?>
suspend fun login(email: String, password: String) : Result <UserDto>
//suspend fun isUserExist(email: String): Result<Boolean?>
suspend fun login(login: String, password: String) : Result <UserDto>
}

@ -3,10 +3,10 @@ package ru.myitschool.work.domain.auth
import javax.inject.Inject
class IsUserExistUseCase (
/*class IsUserExistUseCase (
private val authRepo : AuthRepo
) {
suspend operator fun invoke(email : String) : Result<Boolean?> {
return authRepo.isUserExist(email)
}
}
}*/

@ -5,7 +5,7 @@ import ru.myitschool.work.data.user.UserDto
class LoginUseCase(
private val authRepo : AuthRepo
){
suspend operator fun invoke(email : String, password : String) : Result<UserDto> {
return authRepo.login(email, password)
suspend operator fun invoke(login : String, password : String) : Result<UserDto> {
return authRepo.login(login, password)
}
}

@ -1,18 +0,0 @@
package ru.sicampus.bootcamp2025.data
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
object Network {
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
isLenient = true
ignoreUnknownKeys = true
})
}
}
}

@ -0,0 +1,12 @@
package ru.myitschool.work.domain.user
import kotlinx.serialization.SerialName
data class EntranceEntity(
val login : String,
var name: String,
var enterAt: String,
var enterType: String,
) {
}

@ -0,0 +1,16 @@
package ru.myitschool.work.domain.user
import ru.myitschool.work.data.auth.AuthStorageDataSource
import ru.myitschool.work.data.user.UserDto
class GetUserUseCase(
private val repo: UserRepo,
private val authStorageDataSource: AuthStorageDataSource
) {
suspend fun getUserFromStorage() : UserDto? {
return authStorageDataSource.userInfo
}
suspend operator fun invoke() = repo.getUser(getUserFromStorage()?.login!!)
suspend fun getEntrancesList() = repo.getEntrancesList(getUserFromStorage()?.login!!)
}

@ -8,7 +8,7 @@ data class UserEntity(
var name: String,
var avatarUrl: String?,
val position : String,
var lastEntry : String,
var lastEntry : String?,
val authorities : String
)

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.user
interface UserRepo {
suspend fun getUser(login: String) : Result<UserEntity>
suspend fun getEntrancesList(login : String) : Result<List<EntranceEntity>>
}

@ -1,5 +1,6 @@
package ru.myitschool.work.ui
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
@ -17,22 +18,28 @@ import ru.myitschool.work.ui.qr.scan.QrScanFragment
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint
class RootActivity : AppCompatActivity() {
@SuppressLint("ResourceType")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_root)
setContentView(R.layout.fragment_profile)
val userRole = intent.getStringExtra("USER_ROLE")
/*val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
val navController = navHostFragment.navController*/
if (navHostFragment != null) {
val navController = navHostFragment.navController
navController.graph = navController.createGraph(
startDestination = LoginDestination
) {
fragment<LoginFragment, LoginDestination>()
fragment<QrScanFragment, QrScanDestination>()
}
}
/*if (userRole == "ROLE_ADMIN") {
bottomNavigationView.menu.clear()
bottomNavigationView.inflateMenu(R.menu.bottom_menu_admin)
navController.setGraph(R.navigation.main_admin_nav_graph)
} else {
bottomNavigationView.menu.clear()
bottomNavigationView.inflateMenu(R.menu.bottom_menu)
navController.setGraph(R.navigation.main_nav_graph)
}*/
onBackPressedDispatcher.addCallback(
this,
@ -44,7 +51,7 @@ class RootActivity : AppCompatActivity() {
)
}
override fun onSupportNavigateUp(): Boolean {
/*override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
val popBackResult = if (navController.previousBackStackEntry != null) {
navController.popBackStack()
@ -53,5 +60,5 @@ class RootActivity : AppCompatActivity() {
}
return popBackResult || super.onSupportNavigateUp()
}*/
}
}

@ -65,7 +65,7 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
override fun afterTextChanged(p0: Editable?) {
viewModel.changeLogin()
}
})

@ -17,14 +17,13 @@ import ru.myitschool.work.R
import ru.myitschool.work.data.auth.AuthNetworkDataSource
import ru.myitschool.work.data.auth.AuthRepoImpl
import ru.myitschool.work.data.auth.AuthStorageDataSource
import ru.myitschool.work.domain.auth.IsUserExistUseCase
//import ru.myitschool.work.domain.auth.IsUserExistUseCase
import ru.myitschool.work.domain.auth.LoginUseCase
import javax.inject.Inject
import kotlin.reflect.KClass
class LoginViewModel constructor(
@ApplicationContext private val context: Context,
private val isUserExistUseCase: IsUserExistUseCase,
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val _state = MutableStateFlow<State>(getStateShow())
@ -49,44 +48,23 @@ class LoginViewModel constructor(
)
{
viewModelScope.launch {
when (checkUserExistence(login)) {
true -> {
loginUser(login, password)
}
false -> {
updateState(context.getString(R.string.error_invalid_credentials))
}
null -> updateState(context.getString(R.string.error_unknown))
}
loginUser(login, password)
}
}
private suspend fun checkUserExistence(email: String):Boolean?{
return try {
val result = isUserExistUseCase(email)
result.fold(
onSuccess = {isExist -> isExist},
onFailure = {
Log.e("LoginViewModel", "Error checking user existence", it)
null
}
)
} catch (e: Exception) {
Log.e("LoginViewModel", "Error during user existence check", e)
null
}
}
private suspend fun loginUser(email: String, password: String) {
loginUseCase(email, password).fold(
onSuccess = { user ->
println("Login successful")
Log.d("loginViewModel","Login successful")
_userRole.emit(user.authorities)
_navigateToMain.emit(user.authorities)
},
onFailure = { error ->
updateState(error.message ?: context.getString(R.string.error_unknown))
Log.d("errorLoginViewModel","${error.message}")
}
)
}
@ -125,7 +103,6 @@ class LoginViewModel constructor(
)
return LoginViewModel(
application,
isUserExistUseCase = IsUserExistUseCase(authRepoImpl),
loginUseCase = LoginUseCase(authRepoImpl)
) as T
}

@ -0,0 +1,68 @@
package ru.myitschool.work.ui.profile
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.squareup.picasso.Picasso
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentProfileBinding
import ru.myitschool.work.ui.login.EntryActivity
import ru.myitschool.work.utils.collectWithLifecycle
class ProfileFragment : Fragment(R.layout.fragment_profile) {
private var _viewBinding: FragmentProfileBinding? = null
private val viewBinding: FragmentProfileBinding get() = _viewBinding!!
private val viewModel by viewModels<ProfileViewModel> { ProfileViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_viewBinding = FragmentProfileBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
viewBinding.refresh.setOnClickListener { viewModel.clickRefresh() }
viewBinding.refreshForProfile.setOnClickListener { viewModel.clickRefresh() }
viewBinding.logout.setOnClickListener {
val intent = Intent(requireContext(), EntryActivity::class.java)
startActivity(intent)
requireActivity().finish()
}
viewModel.state.collectWithLifecycle(this) { state ->
viewBinding.error.visibility = if (state is ProfileViewModel.State.Error) View.VISIBLE else View.GONE
viewBinding.loading.visibility = if (state is ProfileViewModel.State.Loading) View.VISIBLE else View.GONE
viewBinding.profile.visibility = if (state is ProfileViewModel.State.Show) View.VISIBLE else View.GONE
when(state) {
is ProfileViewModel.State.Loading -> Unit
is ProfileViewModel.State.Show -> {
viewBinding.name.text = state.profileInfo.name
viewBinding.position.text = "Должность: ${state.profileInfo.name}"
if (state.profileInfo.lastEntry == null) viewBinding.lastEntry.text = "Время последнего входа: Нет данных"
else viewBinding.lastEntry.text = "Время последнего входа: ${state.profileInfo.lastEntry}"
Picasso.get().load(state.profileInfo.avatarUrl).resize(100, 100).centerCrop().into(viewBinding.imageView)
//if (state.entrancesList == emptyList())
//viewBinding.recyclerView.visibility = View.GONE
}
is ProfileViewModel.State.Error -> {
viewBinding.errorText.text = state.text
//viewBinding.noData.visibility = View.VISIBLE
}
}
}
}
override fun onDestroyView() {
_viewBinding = null
super.onDestroyView()
}
}

@ -0,0 +1,86 @@
package ru.myitschool.work.ui.profile
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.auth.AuthStorageDataSource
import ru.myitschool.work.domain.user.EntranceEntity
import ru.myitschool.work.domain.user.GetUserUseCase
import ru.myitschool.work.domain.user.UserEntity
import ru.sicampus.bootcamp2025.data.user.UserNetworkDataSource
import ru.sicampus.bootcamp2025.data.user.UserRepoImpl
class ProfileViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow()
init {
updateStateGet()
}
fun clickRefresh() {
updateStateGet()
}
fun updateStateGet() {
viewModelScope.launch {
_state.emit(State.Loading)
val entranceList : List<EntranceEntity> = getUserUseCase.getEntrancesList().fold(
onSuccess = { list ->
list
},
onFailure = {
emptyList()
}
)
_state.emit(
getUserUseCase.invoke().fold(
onSuccess = { data ->
Log.d("uraa", "успех успех ${data.toString()}")
State.Show(data, entranceList)
},
onFailure = { error ->
Log.d("kaput", error.message.toString())
State.Error(error.message.toString())
}
)
)
//_state.emit(State.Error("о нет ошибка ошибка помогите"))
}
}
sealed interface State {
data object Loading: State
data class Show(
val profileInfo: UserEntity,
val entrancesList : List<EntranceEntity>
) : State
data class Error(
val text: String
) : State
}
companion object {
val Factory : ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProfileViewModel(
getUserUseCase = GetUserUseCase(
repo = UserRepoImpl(
userNetworkDataSource = UserNetworkDataSource()
),
authStorageDataSource = AuthStorageDataSource
)
) as T
}
}
}
}

@ -106,7 +106,7 @@
tools:text="Иванова Вера Павловна" />
<TextView
android:id="@+id/textView"
android:id="@+id/position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@ -118,6 +118,7 @@
tools:text="Должность: Разработчик" />
<TextView
android:id="@+id/lastEntry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@ -125,7 +126,7 @@
android:textColor="@color/black"
android:textSize="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/position"
tools:text="Время последнего входа: 12:00 16.09" />
<com.google.android.material.button.MaterialButton
@ -167,6 +168,7 @@
<View
android:id="@+id/view"
android:layout_width="1dp"
android:layout_height="35dp"
android:layout_marginTop="7dp"
@ -189,25 +191,70 @@
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginTop="15dp"
android:layout_marginTop="8dp"
android:elevation="10dp"
android:gravity="center"
android:text="@string/entry_time"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/view2"
app:layout_constraintEnd_toStartOf="@+id/view3"
app:layout_constraintHorizontal_bias="0.51"
app:layout_constraintStart_toStartOf="@+id/view2"
app:layout_constraintTop_toTopOf="@+id/view2"
app:layout_constraintVertical_bias="0.0" />
app:layout_constraintTop_toTopOf="@+id/view2" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginTop="8dp"
android:elevation="10dp"
android:gravity="center"
android:text="@string/entry"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/view"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/view3"
app:layout_constraintTop_toTopOf="@+id/view2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginTop="8dp"
android:elevation="10dp"
android:gravity="center"
android:text="@string/type_of_entrance"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/view"
app:layout_constraintTop_toTopOf="@+id/view2" />
<TextView
android:id="@+id/no_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/no_data"
android:textColor="@color/black"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view2" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/admin_main_nav_graph">
<!--<fragment
android:id="@+id/fragment_profile"
android:name=""
android:label="Profile"
tools:layout="@layout/fragment_profile" />
<fragment
android:id="@+id/fragment_user_list"
android:name="ru.sicampus.bootcamp2025.ui.userList.FreeVolunteersListFragment"
android:label="UserList"
tools:layout="@layout/fragment_free_volunteers_list" />-->
</navigation>

@ -10,4 +10,7 @@
<string name="error_unknown">Непредвиденная ошибка</string>
<string name="entry_history">История входов\n</string>
<string name="entry_time">Время входа\n</string>
<string name="entry">Вход</string>
<string name="type_of_entrance">Тип прохода</string>
<string name="no_data">Нет данных\n</string>
</resources>