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
}
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>> =
merge(
resumeDao.getById(resumeId = resumeId).map { entity ->
@@ -12,5 +12,6 @@ interface ResumeRepository {
suspend fun suggestSkills(query: String): Result<List<String>>
suspend fun createResume(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>>
}
@@ -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(
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()
)
salary = prediction.toSalaryRangeString(),
isPredictionLoading = prediction == null
)
@@ -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(
@@ -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
) {
@@ -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<String>(AppDestination.ResumeEdit.ARG_ID)
?: savedStateHandle.get<String>("id") ?: ""
init {
viewModelScope.launch {
getResumeInfoUseCase(resumeId).collect { result ->
@@ -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
)
}
}
}
@@ -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,
@@ -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<ResumeModel>()
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) {
getResumeInfoUseCase(resumeId).collectRequest(_resumeState)
viewModelScope.launch {
resumeState.collect {
val data = (it as? UIState.Success)?.data
if (data?.prediction == null) {
startPredictionPolling()
}
}
}
}
init {