From 4fadf1bb810772f7fcd39d9d432c13190b0fae63 Mon Sep 17 00:00:00 2001 From: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com> Date: Sun, 23 Nov 2025 04:47:40 +0300 Subject: [PATCH] fixes --- .../ResumeRepositoryImpl.kt | 13 ++++++++ .../interfaces/resumes/ResumeRepository.kt | 1 + .../usecase/resumes/RefreshResumeUseCase.kt | 13 ++++++++ .../dataModels/UIResumeBaseInfo.kt | 8 +++-- .../presentation/navigation/TTasksNavHost.kt | 4 +-- .../EditResumeScreen.kt | 8 ++--- .../EditResumeViewModel.kt | 17 ++++++---- .../presentation/screens/main/MainScreen.kt | 16 +++++---- .../resumeDetails/ResumeDetailsScreen.kt | 27 +++++++++++---- .../resumeDetails/ResumeDetailsViewModel.kt | 33 ++++++++++++++++++- 10 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/RefreshResumeUseCase.kt rename app/src/main/java/com/prodhack/moscow2025/presentation/screens/{resumeDetails => editResume}/EditResumeScreen.kt (77%) rename app/src/main/java/com/prodhack/moscow2025/presentation/screens/{resumeDetails => editResume}/EditResumeViewModel.kt (74%) diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt index f6c69fc..86b9144 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt @@ -91,6 +91,19 @@ class ResumeRepositoryImpl( resumeId } + override suspend fun refreshResume(resumeId: String): Result = + networkRequest { + method = HttpMethod.Get + url { + path("resume", resumeId) + } + }.map { + it.mapToDomain().also { model -> + resumeDao.upsertAll(listOf(it.mapToDB())) + resumeHistoryDao.upsertAll(listOf(ResumeHistoryEntity.fromDomain(model))) + } + } + override fun getResume(resumeId: String): Flow> = merge( resumeDao.getById(resumeId = resumeId).map { entity -> diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt index a8bde93..19d8866 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/interfaces/resumes/ResumeRepository.kt @@ -12,5 +12,6 @@ interface ResumeRepository { suspend fun suggestSkills(query: String): Result> suspend fun createResume(resumeForm: ResumeCreationModel): Result suspend fun updateResume(resumeId: String, resumeForm: ResumeCreationModel): Result + suspend fun refreshResume(resumeId: String): Result fun getResume(resumeId: String): Flow> } diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/RefreshResumeUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/RefreshResumeUseCase.kt new file mode 100644 index 0000000..d0904f0 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/RefreshResumeUseCase.kt @@ -0,0 +1,13 @@ +package com.prodhack.moscow2025.domain.usecase.resumes + +import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository +import com.prodhack.moscow2025.domain.models.ResumeModel +import org.koin.core.annotation.Single + +@Single +class RefreshResumeUseCase( + private val resumeRepository: ResumeRepository +) { + suspend operator fun invoke(resumeId: String): Result = + resumeRepository.refreshResume(resumeId) +} diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt index 739dc00..945e826 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt @@ -6,11 +6,13 @@ import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString data class UIResumeBaseInfo( val id: String, val positionName: String, - val salary: String + val salary: String, + val isPredictionLoading: Boolean ) fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo( id = id, positionName = position, - salary = prediction.toSalaryRangeString() -) \ No newline at end of file + salary = prediction.toSalaryRangeString(), + isPredictionLoading = prediction == null +) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt index c1c157a..1ad9a41 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt @@ -5,7 +5,6 @@ import android.os.Bundle import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.core.os.bundleOf import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -17,11 +16,10 @@ import com.prodhack.moscow2025.presentation.screens.login.LoginScreen import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen -import com.prodhack.moscow2025.presentation.screens.resumeDetails.EditResumeScreen +import com.prodhack.moscow2025.presentation.screens.editResume.EditResumeScreen import com.prodhack.moscow2025.presentation.screens.resumeHistory.ResumeHistoryScreen import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope -import org.koin.compose.viewmodel.koinActivityViewModel @Composable fun TTasksNavHost( diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeScreen.kt similarity index 77% rename from app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeScreen.kt rename to app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeScreen.kt index b6fbe20..cfe52c8 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeScreen.kt @@ -1,4 +1,4 @@ -package com.prodhack.moscow2025.presentation.screens.resumeDetails +package com.prodhack.moscow2025.presentation.screens.editResume import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry @@ -11,11 +11,7 @@ import org.koin.core.parameter.parametersOf @Composable fun ErrorCollectorScope.EditResumeScreen( navBackStackEntry: NavBackStackEntry, - viewModel: EditResumeViewModel = koinViewModel { - parametersOf( - navBackStackEntry.arguments?.getString(AppDestination.ResumeEdit.ARG_ID, "") ?: "" - ) - }, + viewModel: EditResumeViewModel = koinViewModel(), openResumeDetails: (String) -> Unit, goBack: () -> Unit ) { diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeViewModel.kt similarity index 74% rename from app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeViewModel.kt rename to app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeViewModel.kt index d2ff7d1..a3306d3 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/editResume/EditResumeViewModel.kt @@ -1,14 +1,15 @@ -package com.prodhack.moscow2025.presentation.screens.resumeDetails +package com.prodhack.moscow2025.presentation.screens.editResume +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.prodhack.moscow2025.domain.usecase.resumes.PostResumeUseCase -import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase -import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.PostResumeUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase +import com.prodhack.moscow2025.presentation.navigation.AppDestination import com.prodhack.moscow2025.presentation.screens.createResume.CreateResumeViewModel import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel -import org.koin.core.annotation.Provided @KoinViewModel class EditResumeViewModel( @@ -16,12 +17,16 @@ class EditResumeViewModel( suggestSkillsUseCase: SuggestSkillsUseCase, validateDataUseCase: ValidateFieldsUseCase, postResumeUseCase: PostResumeUseCase, - @Provided private val resumeId: String + savedStateHandle: SavedStateHandle ) : CreateResumeViewModel( suggestSkillsUseCase = suggestSkillsUseCase, validateDataUseCase = validateDataUseCase, postResumeUseCase = postResumeUseCase ) { + private val resumeId: String = + savedStateHandle.get(AppDestination.ResumeEdit.ARG_ID) + ?: savedStateHandle.get("id") ?: "" + init { viewModelScope.launch { getResumeInfoUseCase(resumeId).collect { result -> diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt index 8d4fe6c..d4a4a89 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt @@ -199,12 +199,16 @@ fun ResumeShortInfoCard( style = typography.labelLarge, fontSize = 18.sp ) - Text( - info.salary, - style = typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - fontSize = 18.sp - ) + if (info.isPredictionLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + } else { + Text( + info.salary, + style = typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + fontSize = 18.sp + ) + } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt index d1c46e9..2c65073 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -54,14 +55,12 @@ import org.koin.core.parameter.parametersOf @Composable fun ErrorCollectorScope.ResumeDetailsScreen( navBackStackEntry: NavBackStackEntry, -// onHistory: () -> Unit, viewModel: ResumeDetailsViewModel = koinViewModel { parametersOf( navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: "" ) } ) { - val context = LocalContext.current val resumeState by viewModel.resumeState.collectAsStateWithCallbacks() @@ -172,11 +171,25 @@ private fun ResumeDetailsContent( ) { Spacer(modifier = Modifier.height(Paddings.large)) SectionContainer { - Text( - "${resume.position} • ${resume.prediction.toSalaryRangeString()}", - style = typography.titleLarge, - fontSize = 28.sp - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Paddings.small) + ) { + Text( + resume.position, + style = typography.titleLarge, + fontSize = 28.sp + ) + if (resume.prediction == null) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + } else { + Text( + resume.prediction.toSalaryRangeString(), + style = typography.titleMedium, + fontSize = 18.sp + ) + } + } Text( text = resume.city, style = typography.labelLarge, diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt index f4bc183..e7a5434 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt @@ -2,8 +2,12 @@ package com.prodhack.moscow2025.presentation.screens.resumeDetails import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase +import com.prodhack.moscow2025.domain.usecase.resumes.RefreshResumeUseCase import com.prodhack.moscow2025.presentation.utils.UIState import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.StateFlow import org.koin.android.annotation.KoinViewModel import org.koin.core.annotation.Provided @@ -11,13 +15,40 @@ import org.koin.core.annotation.Provided @KoinViewModel class ResumeDetailsViewModel( @Provided resumeId: String, - private val getResumeInfoUseCase: GetResumeInfoUseCase + private val getResumeInfoUseCase: GetResumeInfoUseCase, + private val refreshResumeUseCase: RefreshResumeUseCase ) : BaseViewModel() { private val _resumeState = MutableUIStateFlow() val resumeState: StateFlow> = _resumeState + private val id = resumeId + private var pollingStarted = false + + private fun startPredictionPolling() { + if (pollingStarted) return + pollingStarted = true + viewModelScope.launch { + while (true) { + val current = (_resumeState.value as? UIState.Success)?.data + if (current?.prediction?.first != null || current?.prediction?.second != null) break + delay(2000) + val refreshed = refreshResumeUseCase(id) + if (refreshed.isSuccess) { + _resumeState.value = UIState.Success(refreshed.getOrNull()!!) + } + } + } + } fun loadResume(resumeId: String) { getResumeInfoUseCase(resumeId).collectRequest(_resumeState) + viewModelScope.launch { + resumeState.collect { + val data = (it as? UIState.Success)?.data + if (data?.prediction == null) { + startPredictionPolling() + } + } + } } init {