feat: edit resume

This commit is contained in:
MaximOksiuta
2025-11-23 02:31:04 +03:00
parent 84276397de
commit 962e513856
12 changed files with 157 additions and 39 deletions
@@ -217,9 +217,9 @@ data class ResumeCreateDTO(
val position: String, val position: String,
@SerialName("location") @SerialName("location")
val city: String, val city: String,
val experience: List<ExperienceDTO>, val experience: List<ExperienceDTO>? = null,
val education: List<EducationDTO>, val education: List<EducationDTO>? = null,
val project: List<ProjectDTO>, val project: List<ProjectDTO>? = null,
) )
fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO( fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO(
@@ -74,6 +74,22 @@ class ResumeRepositoryImpl(
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
}.map { it.resumeId } }.map { it.resumeId }
override suspend fun updateResume(
resumeId: String,
resumeForm: ResumeCreationModel
): Result<String> = networkRequest<ResumeCreateDTO> {
method = HttpMethod.Patch
url {
path("resume", resumeId)
}
setBody(resumeForm.mapToData())
contentType(ContentType.Application.Json)
}.map {
resumeId
}
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 ->
@@ -10,6 +10,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>
fun getResume(resumeId: String): Flow<Result<ResumeModel>> fun getResume(resumeId: String): Flow<Result<ResumeModel>>
} }
@@ -5,9 +5,16 @@ import com.prodhack.moscow2025.domain.models.ResumeCreationModel
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@Single @Single
class CreateResumeUseCase( class PostResumeUseCase(
private val resumeRepository: ResumeRepository private val resumeRepository: ResumeRepository
) { ) {
suspend operator fun invoke(resumeForm: ResumeCreationModel): Result<String> = suspend operator fun invoke(
resumeRepository.createResume(resumeForm) resumeForm: ResumeCreationModel,
isNew: Boolean,
resumeId: String?
): Result<String> =
if (isNew) resumeRepository.createResume(resumeForm) else resumeRepository.updateResume(
resumeId!!,
resumeForm
)
} }
@@ -22,5 +22,8 @@ sealed class AppDestination(val route: String) {
} }
data object ResumeCreation: AppDestination("resume/creation") data object ResumeCreation: AppDestination("resume/creation")
}
data object ResumeEdit : AppDestination("resume/edit") {
const val ARG_ID = "id"
}
}
@@ -17,6 +17,7 @@ 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.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 import org.koin.compose.viewmodel.koinActivityViewModel
@@ -124,6 +125,18 @@ fun TTasksNavHost(
}) })
}) })
} }
composable(AppDestination.ResumeEdit.route) {
EditResumeScreen(navBackStackEntry = it, goBack = {
navController.popBackStack()
}, openResumeDetails = { id ->
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
putString(AppDestination.ResumeDetails.ARG_ID, id)
}
)
}
)
}
} }
} }
} }
@@ -52,7 +52,9 @@ import org.koin.androidx.compose.koinViewModel
fun ErrorCollectorScope.CreateResumeScreen( fun ErrorCollectorScope.CreateResumeScreen(
goBack: () -> Unit, goBack: () -> Unit,
openResumeDetails: (String) -> Unit, openResumeDetails: (String) -> Unit,
viewModel: CreateResumeViewModel = koinViewModel() viewModel: CreateResumeViewModel = koinViewModel(),
title: String = "Новое резюме",
submitButtonText: String = "Узнать свою ЗП"
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
@@ -79,7 +81,7 @@ fun ErrorCollectorScope.CreateResumeScreen(
tint = colorScheme.onBackground, tint = colorScheme.onBackground,
contentDescription = "go back" contentDescription = "go back"
) )
Text(text = "Новое резюме", style = typography.titleLarge, fontSize = 24.sp) Text(text = title, style = typography.titleLarge, fontSize = 24.sp)
Spacer(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.size(24.dp))
} }
Column( Column(
@@ -300,7 +302,7 @@ fun ErrorCollectorScope.CreateResumeScreen(
} }
BigButton( BigButton(
onClick = viewModel::submit, onClick = viewModel::submit,
buttonText = "Узнать свою зарплату", buttonText = submitButtonText,
isLoading = resumeFillState.value.isLoading isLoading = resumeFillState.value.isLoading
) )
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
@@ -9,9 +9,10 @@ import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.Project import com.prodhack.moscow2025.domain.models.Project
import com.prodhack.moscow2025.domain.models.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeCreationModel
import com.prodhack.moscow2025.domain.models.ResumeField import com.prodhack.moscow2025.domain.models.ResumeField
import com.prodhack.moscow2025.domain.models.ResumeModel
import com.prodhack.moscow2025.domain.models.WorkExperience import com.prodhack.moscow2025.domain.models.WorkExperience
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
import com.prodhack.moscow2025.domain.usecase.resumes.CreateResumeUseCase import com.prodhack.moscow2025.domain.usecase.resumes.PostResumeUseCase
import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase
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
@@ -35,16 +36,18 @@ data class ResumeFormState(
) )
@KoinViewModel @KoinViewModel
class CreateResumeViewModel( open class CreateResumeViewModel(
private val suggestSkillsUseCase: SuggestSkillsUseCase, private val suggestSkillsUseCase: SuggestSkillsUseCase,
private val validateDataUseCase: ValidateFieldsUseCase, private val validateDataUseCase: ValidateFieldsUseCase,
private val createResumeUseCase: CreateResumeUseCase private val postResumeUseCase: PostResumeUseCase
) : BaseViewModel() { ) : BaseViewModel() {
private val _formStateFillResume = MutableStateFlow(ResumeFormState()) private val _formStateFillResume = MutableStateFlow(ResumeFormState())
val formStateFillResume: StateFlow<ResumeFormState> = _formStateFillResume val formStateFillResume: StateFlow<ResumeFormState> = _formStateFillResume
private val _resumeFillState = MutableUIStateFlow<String>() private val _resumeFillState = MutableUIStateFlow<String>()
val resumeFillState: StateFlow<UIState<String>> = _resumeFillState val resumeFillState: StateFlow<UIState<String>> = _resumeFillState
private var prefilled = false
private var currId: String? = null
// Simple fields // Simple fields
fun onAboutChange(value: String) { fun onAboutChange(value: String) {
@@ -295,6 +298,24 @@ class CreateResumeViewModel(
} }
} }
fun prefill(resume: ResumeModel) {
if (prefilled) return
prefilled = true
currId = resume.id
_formStateFillResume.update {
it.copy(
about = resume.about,
position = resume.position,
experience = resume.experienceType,
keySkills = resume.skills.toSet(),
city = resume.city,
workExperience = resume.experience,
education = resume.education,
projects = resume.projects
)
}
}
fun submit() { fun submit() {
viewModelScope.launch { viewModelScope.launch {
val validation = validateDataUseCase.validateResume( val validation = validateDataUseCase.validateResume(
@@ -315,7 +336,7 @@ class CreateResumeViewModel(
_resumeFillState.emit(UIState.Loading()) _resumeFillState.emit(UIState.Loading())
val result = createResumeUseCase( val result = postResumeUseCase(
with(_formStateFillResume.value) { with(_formStateFillResume.value) {
ResumeCreationModel( ResumeCreationModel(
position = position, position = position,
@@ -327,8 +348,9 @@ class CreateResumeViewModel(
education = education, education = education,
projects = projects projects = projects
) )
} },
isNew = prefilled.not(),
resumeId = currId
) )
result.collectRequest(_resumeFillState) result.collectRequest(_resumeFillState)
} }
@@ -16,10 +16,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -219,5 +217,3 @@ fun ResumeShortInfoCard(
} }
} }
} }
@@ -0,0 +1,29 @@
package com.prodhack.moscow2025.presentation.screens.resumeDetails
import androidx.compose.runtime.Composable
import androidx.navigation.NavBackStackEntry
import com.prodhack.moscow2025.presentation.navigation.AppDestination
import com.prodhack.moscow2025.presentation.screens.createResume.CreateResumeScreen
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable
fun ErrorCollectorScope.EditResumeScreen(
navBackStackEntry: NavBackStackEntry,
viewModel: EditResumeViewModel = koinViewModel {
parametersOf(
navBackStackEntry.arguments?.getString(AppDestination.ResumeEdit.ARG_ID, "") ?: ""
)
},
openResumeDetails: (String) -> Unit,
goBack: () -> Unit
) {
CreateResumeScreen(
goBack = goBack,
openResumeDetails = openResumeDetails,
viewModel = viewModel,
title = "Изменить резюме",
submitButtonText = "Пересчитать"
)
}
@@ -0,0 +1,32 @@
package com.prodhack.moscow2025.presentation.screens.resumeDetails
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.presentation.screens.createResume.CreateResumeViewModel
import kotlinx.coroutines.launch
import org.koin.android.annotation.KoinViewModel
import org.koin.core.annotation.Provided
@KoinViewModel
class EditResumeViewModel(
private val getResumeInfoUseCase: GetResumeInfoUseCase,
suggestSkillsUseCase: SuggestSkillsUseCase,
validateDataUseCase: ValidateFieldsUseCase,
postResumeUseCase: PostResumeUseCase,
@Provided private val resumeId: String
) : CreateResumeViewModel(
suggestSkillsUseCase = suggestSkillsUseCase,
validateDataUseCase = validateDataUseCase,
postResumeUseCase = postResumeUseCase
) {
init {
viewModelScope.launch {
getResumeInfoUseCase(resumeId).collect { result ->
result.getOrNull()?.let { prefill(it) }
}
}
}
}
@@ -1,6 +1,5 @@
package com.prodhack.moscow2025.presentation.screens.resumeDetails package com.prodhack.moscow2025.presentation.screens.resumeDetails
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -19,10 +18,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -35,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import android.os.Bundle
import com.prodhack.moscow2025.R import com.prodhack.moscow2025.R
import com.prodhack.moscow2025.domain.models.Education import com.prodhack.moscow2025.domain.models.Education
import com.prodhack.moscow2025.domain.models.Project import com.prodhack.moscow2025.domain.models.Project
@@ -49,14 +47,14 @@ import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
import com.prodhack.moscow2025.presentation.utils.ui.placeholders.ErrorPlaceholder import com.prodhack.moscow2025.presentation.utils.ui.placeholders.ErrorPlaceholder
import com.prodhack.moscow2025.presentation.utils.ui.placeholders.LoadingPlaceholder import com.prodhack.moscow2025.presentation.utils.ui.placeholders.LoadingPlaceholder
import com.prodhack.moscow2025.presentation.navigation.navigate
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@Composable @Composable
fun ErrorCollectorScope.ResumeDetailsScreen( fun ErrorCollectorScope.ResumeDetailsScreen(
navBackStackEntry: NavBackStackEntry, navBackStackEntry: NavBackStackEntry,
onEditResume: (String) -> Unit, // onHistory: () -> Unit,
onHistory: () -> Unit,
viewModel: ResumeDetailsViewModel = koinViewModel { viewModel: ResumeDetailsViewModel = koinViewModel {
parametersOf( parametersOf(
navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: "" navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""
@@ -89,18 +87,18 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
ResumeDetailsContent( ResumeDetailsContent(
resume = resume, resume = resume,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onHistory = onHistory onHistory = {}
) )
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(bottom = Paddings.large), .padding(bottom = Paddings.large),
onClick = { onClick = {
onEditResume( navController.navigate(
navBackStackEntry.arguments?.getString( AppDestination.ResumeEdit.route,
AppDestination.ResumeDetails.ARG_ID, Bundle().apply {
"" putString(AppDestination.ResumeEdit.ARG_ID, resume.id)
) ?: "" }
) )
}, },
icon = { icon = {