This commit is contained in:
MaximOksiuta
2025-11-23 04:47:40 +03:00
parent ee4a560b53
commit 4fadf1bb81
10 changed files with 108 additions and 32 deletions
@@ -91,6 +91,19 @@ class ResumeRepositoryImpl(
resumeId resumeId
} }
override suspend fun refreshResume(resumeId: String): Result<ResumeModel> =
networkRequest<ResumeDTO> {
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<Result<ResumeModel>> = override fun getResume(resumeId: String): Flow<Result<ResumeModel>> =
merge( merge(
resumeDao.getById(resumeId = resumeId).map { entity -> resumeDao.getById(resumeId = resumeId).map { entity ->
@@ -12,5 +12,6 @@ interface ResumeRepository {
suspend fun suggestSkills(query: String): Result<List<String>> suspend fun suggestSkills(query: String): Result<List<String>>
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String> suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
suspend fun updateResume(resumeId: String, resumeForm: ResumeCreationModel): Result<String> suspend fun updateResume(resumeId: String, resumeForm: ResumeCreationModel): Result<String>
suspend fun refreshResume(resumeId: String): Result<ResumeModel>
fun getResume(resumeId: String): Flow<Result<ResumeModel>> fun getResume(resumeId: String): Flow<Result<ResumeModel>>
} }
@@ -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<ResumeModel> =
resumeRepository.refreshResume(resumeId)
}
@@ -6,11 +6,13 @@ import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
data class UIResumeBaseInfo( data class UIResumeBaseInfo(
val id: String, val id: String,
val positionName: String, val positionName: String,
val salary: String val salary: String,
val isPredictionLoading: Boolean
) )
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo( fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
id = id, id = id,
positionName = position, positionName = position,
salary = prediction.toSalaryRangeString() salary = prediction.toSalaryRangeString(),
isPredictionLoading = prediction == null
) )
@@ -5,7 +5,6 @@ import android.os.Bundle
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable 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.profile.ProfileScreen
import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen 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.screens.resumeHistory.ResumeHistoryScreen
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
import org.koin.compose.viewmodel.koinActivityViewModel
@Composable @Composable
fun TTasksNavHost( fun TTasksNavHost(
@@ -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.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
@@ -11,11 +11,7 @@ import org.koin.core.parameter.parametersOf
@Composable @Composable
fun ErrorCollectorScope.EditResumeScreen( fun ErrorCollectorScope.EditResumeScreen(
navBackStackEntry: NavBackStackEntry, navBackStackEntry: NavBackStackEntry,
viewModel: EditResumeViewModel = koinViewModel { viewModel: EditResumeViewModel = koinViewModel(),
parametersOf(
navBackStackEntry.arguments?.getString(AppDestination.ResumeEdit.ARG_ID, "") ?: ""
)
},
openResumeDetails: (String) -> Unit, openResumeDetails: (String) -> Unit,
goBack: () -> Unit goBack: () -> Unit
) { ) {
@@ -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 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.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 com.prodhack.moscow2025.presentation.screens.createResume.CreateResumeViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.annotation.KoinViewModel import org.koin.android.annotation.KoinViewModel
import org.koin.core.annotation.Provided
@KoinViewModel @KoinViewModel
class EditResumeViewModel( class EditResumeViewModel(
@@ -16,12 +17,16 @@ class EditResumeViewModel(
suggestSkillsUseCase: SuggestSkillsUseCase, suggestSkillsUseCase: SuggestSkillsUseCase,
validateDataUseCase: ValidateFieldsUseCase, validateDataUseCase: ValidateFieldsUseCase,
postResumeUseCase: PostResumeUseCase, postResumeUseCase: PostResumeUseCase,
@Provided private val resumeId: String savedStateHandle: SavedStateHandle
) : CreateResumeViewModel( ) : CreateResumeViewModel(
suggestSkillsUseCase = suggestSkillsUseCase, suggestSkillsUseCase = suggestSkillsUseCase,
validateDataUseCase = validateDataUseCase, validateDataUseCase = validateDataUseCase,
postResumeUseCase = postResumeUseCase postResumeUseCase = postResumeUseCase
) { ) {
private val resumeId: String =
savedStateHandle.get<String>(AppDestination.ResumeEdit.ARG_ID)
?: savedStateHandle.get<String>("id") ?: ""
init { init {
viewModelScope.launch { viewModelScope.launch {
getResumeInfoUseCase(resumeId).collect { result -> getResumeInfoUseCase(resumeId).collect { result ->
@@ -199,12 +199,16 @@ fun ResumeShortInfoCard(
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp fontSize = 18.sp
) )
Text( if (info.isPredictionLoading) {
info.salary, CircularProgressIndicator(modifier = Modifier.size(18.dp))
style = typography.titleMedium, } else {
color = MaterialTheme.colorScheme.primary, Text(
fontSize = 18.sp info.salary,
) style = typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
fontSize = 18.sp
)
}
} }
} }
@@ -21,6 +21,7 @@ import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -54,14 +55,12 @@ import org.koin.core.parameter.parametersOf
@Composable @Composable
fun ErrorCollectorScope.ResumeDetailsScreen( fun ErrorCollectorScope.ResumeDetailsScreen(
navBackStackEntry: NavBackStackEntry, navBackStackEntry: NavBackStackEntry,
// onHistory: () -> Unit,
viewModel: ResumeDetailsViewModel = koinViewModel { viewModel: ResumeDetailsViewModel = koinViewModel {
parametersOf( parametersOf(
navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: "" navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""
) )
} }
) { ) {
val context = LocalContext.current
val resumeState by viewModel.resumeState.collectAsStateWithCallbacks() val resumeState by viewModel.resumeState.collectAsStateWithCallbacks()
@@ -172,11 +171,25 @@ private fun ResumeDetailsContent(
) { ) {
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
SectionContainer { SectionContainer {
Text( Row(
"${resume.position}${resume.prediction.toSalaryRangeString()}", verticalAlignment = Alignment.CenterVertically,
style = typography.titleLarge, horizontalArrangement = Arrangement.spacedBy(Paddings.small)
fontSize = 28.sp ) {
) 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(
text = resume.city, text = resume.city,
style = typography.labelLarge, style = typography.labelLarge,
@@ -2,8 +2,12 @@ package com.prodhack.moscow2025.presentation.screens.resumeDetails
import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase 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.UIState
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel 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 kotlinx.coroutines.flow.StateFlow
import org.koin.android.annotation.KoinViewModel import org.koin.android.annotation.KoinViewModel
import org.koin.core.annotation.Provided import org.koin.core.annotation.Provided
@@ -11,13 +15,40 @@ import org.koin.core.annotation.Provided
@KoinViewModel @KoinViewModel
class ResumeDetailsViewModel( class ResumeDetailsViewModel(
@Provided resumeId: String, @Provided resumeId: String,
private val getResumeInfoUseCase: GetResumeInfoUseCase private val getResumeInfoUseCase: GetResumeInfoUseCase,
private val refreshResumeUseCase: RefreshResumeUseCase
) : BaseViewModel() { ) : BaseViewModel() {
private val _resumeState = MutableUIStateFlow<ResumeModel>() private val _resumeState = MutableUIStateFlow<ResumeModel>()
val resumeState: StateFlow<UIState<ResumeModel>> = _resumeState val resumeState: StateFlow<UIState<ResumeModel>> = _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) { fun loadResume(resumeId: String) {
getResumeInfoUseCase(resumeId).collectRequest(_resumeState) getResumeInfoUseCase(resumeId).collectRequest(_resumeState)
viewModelScope.launch {
resumeState.collect {
val data = (it as? UIState.Success)?.data
if (data?.prediction == null) {
startPredictionPolling()
}
}
}
} }
init { init {