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,
@SerialName("location")
val city: String,
val experience: List<ExperienceDTO>,
val education: List<EducationDTO>,
val project: List<ProjectDTO>,
val experience: List<ExperienceDTO>? = null,
val education: List<EducationDTO>? = null,
val project: List<ProjectDTO>? = null,
)
fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO(
@@ -74,6 +74,22 @@ class ResumeRepositoryImpl(
contentType(ContentType.Application.Json)
}.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>> =
merge(
resumeDao.getById(resumeId = resumeId).map { entity ->
@@ -10,6 +10,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>
fun getResume(resumeId: String): Flow<Result<ResumeModel>>
}
@@ -5,9 +5,16 @@ import com.prodhack.moscow2025.domain.models.ResumeCreationModel
import org.koin.core.annotation.Single
@Single
class CreateResumeUseCase(
class PostResumeUseCase(
private val resumeRepository: ResumeRepository
) {
suspend operator fun invoke(resumeForm: ResumeCreationModel): Result<String> =
resumeRepository.createResume(resumeForm)
suspend operator fun invoke(
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 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.register.RegisterScreen
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.ErrorCollectorScope
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(
goBack: () -> Unit,
openResumeDetails: (String) -> Unit,
viewModel: CreateResumeViewModel = koinViewModel()
viewModel: CreateResumeViewModel = koinViewModel(),
title: String = "Новое резюме",
submitButtonText: String = "Узнать свою ЗП"
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
@@ -79,7 +81,7 @@ fun ErrorCollectorScope.CreateResumeScreen(
tint = colorScheme.onBackground,
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))
}
Column(
@@ -295,14 +297,14 @@ fun ErrorCollectorScope.CreateResumeScreen(
Spacer(modifier = Modifier.height(Paddings.large))
val resumeFillState = viewModel.resumeFillState.collectAsStateWithCallbacks {
openResumeDetails(it)
}
BigButton(
onClick = viewModel::submit,
buttonText = "Узнать свою зарплату",
isLoading = resumeFillState.value.isLoading
)
val resumeFillState = viewModel.resumeFillState.collectAsStateWithCallbacks {
openResumeDetails(it)
}
BigButton(
onClick = viewModel::submit,
buttonText = submitButtonText,
isLoading = resumeFillState.value.isLoading
)
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.ResumeCreationModel
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.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.presentation.utils.UIState
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
@@ -35,16 +36,18 @@ data class ResumeFormState(
)
@KoinViewModel
class CreateResumeViewModel(
open class CreateResumeViewModel(
private val suggestSkillsUseCase: SuggestSkillsUseCase,
private val validateDataUseCase: ValidateFieldsUseCase,
private val createResumeUseCase: CreateResumeUseCase
private val postResumeUseCase: PostResumeUseCase
) : BaseViewModel() {
private val _formStateFillResume = MutableStateFlow(ResumeFormState())
val formStateFillResume: StateFlow<ResumeFormState> = _formStateFillResume
private val _resumeFillState = MutableUIStateFlow<String>()
val resumeFillState: StateFlow<UIState<String>> = _resumeFillState
private var prefilled = false
private var currId: String? = null
// Simple fields
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() {
viewModelScope.launch {
val validation = validateDataUseCase.validateResume(
@@ -315,7 +336,7 @@ class CreateResumeViewModel(
_resumeFillState.emit(UIState.Loading())
val result = createResumeUseCase(
val result = postResumeUseCase(
with(_formStateFillResume.value) {
ResumeCreationModel(
position = position,
@@ -327,8 +348,9 @@ class CreateResumeViewModel(
education = education,
projects = projects
)
}
},
isNew = prefilled.not(),
resumeId = currId
)
result.collectRequest(_resumeFillState)
}
@@ -16,10 +16,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
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
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -19,10 +18,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.sp
import androidx.navigation.NavBackStackEntry
import android.os.Bundle
import com.prodhack.moscow2025.R
import com.prodhack.moscow2025.domain.models.Education
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.placeholders.ErrorPlaceholder
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.core.parameter.parametersOf
@Composable
fun ErrorCollectorScope.ResumeDetailsScreen(
navBackStackEntry: NavBackStackEntry,
onEditResume: (String) -> Unit,
onHistory: () -> Unit,
// onHistory: () -> Unit,
viewModel: ResumeDetailsViewModel = koinViewModel {
parametersOf(
navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""
@@ -89,18 +87,18 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
ResumeDetailsContent(
resume = resume,
onBack = { navController.popBackStack() },
onHistory = onHistory
onHistory = {}
)
ExtendedFloatingActionButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = Paddings.large),
onClick = {
onEditResume(
navBackStackEntry.arguments?.getString(
AppDestination.ResumeDetails.ARG_ID,
""
) ?: ""
navController.navigate(
AppDestination.ResumeEdit.route,
Bundle().apply {
putString(AppDestination.ResumeEdit.ARG_ID, resume.id)
}
)
},
icon = {