From 962e513856a3b95512c68c8a17e5b224f194e4bb Mon Sep 17 00:00:00 2001 From: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:31:04 +0300 Subject: [PATCH] feat: edit resume --- .../moscow2025/data/dto/ResumeDtos.kt | 6 ++-- .../ResumeRepositoryImpl.kt | 16 +++++++++ .../interfaces/resumes/ResumeRepository.kt | 2 +- ...eResumeUseCase.kt => PostResumeUseCase.kt} | 13 +++++-- .../presentation/navigation/AppDestination.kt | 5 ++- .../presentation/navigation/TTasksNavHost.kt | 13 +++++++ .../createResume/CreateResumeScreen.kt | 22 ++++++------ .../createResume/CreateResumeViewModel.kt | 34 +++++++++++++++---- .../presentation/screens/main/MainScreen.kt | 4 --- .../screens/resumeDetails/EditResumeScreen.kt | 29 ++++++++++++++++ .../resumeDetails/EditResumeViewModel.kt | 32 +++++++++++++++++ .../resumeDetails/ResumeDetailsScreen.kt | 20 +++++------ 12 files changed, 157 insertions(+), 39 deletions(-) rename app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/{CreateResumeUseCase.kt => PostResumeUseCase.kt} (52%) create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeScreen.kt create mode 100644 app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeViewModel.kt diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt index 74eb925..0734165 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt @@ -217,9 +217,9 @@ data class ResumeCreateDTO( val position: String, @SerialName("location") val city: String, - val experience: List, - val education: List, - val project: List, + val experience: List? = null, + val education: List? = null, + val project: List? = null, ) fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO( 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 08bf6af..f621334 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 @@ -74,6 +74,22 @@ class ResumeRepositoryImpl( contentType(ContentType.Application.Json) }.map { it.resumeId } + override suspend fun updateResume( + resumeId: String, + resumeForm: ResumeCreationModel + ): Result = networkRequest { + method = HttpMethod.Patch + + url { + path("resume", resumeId) + } + + setBody(resumeForm.mapToData()) + contentType(ContentType.Application.Json) + }.map { + resumeId + } + 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 88aad6e..a586a05 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 @@ -10,6 +10,6 @@ interface ResumeRepository { suspend fun suggestSkills(query: String): Result> suspend fun createResume(resumeForm: ResumeCreationModel): Result - + suspend fun updateResume(resumeId: String, resumeForm: ResumeCreationModel): Result fun getResume(resumeId: String): Flow> } diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/PostResumeUseCase.kt similarity index 52% rename from app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt rename to app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/PostResumeUseCase.kt index 40ba678..db72917 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/CreateResumeUseCase.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/PostResumeUseCase.kt @@ -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 = - resumeRepository.createResume(resumeForm) + suspend operator fun invoke( + resumeForm: ResumeCreationModel, + isNew: Boolean, + resumeId: String? + ): Result = + if (isNew) resumeRepository.createResume(resumeForm) else resumeRepository.updateResume( + resumeId!!, + resumeForm + ) } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt index d4d2e02..8e604fa 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/AppDestination.kt @@ -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" + } +} 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 3d2a30b..fae15cb 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 @@ -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) + } + ) + } + ) + } } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt index cfb5eb2..c9e9eba 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt @@ -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)) } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt index 1081753..2f74221 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt @@ -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 = _formStateFillResume private val _resumeFillState = MutableUIStateFlow() val resumeFillState: StateFlow> = _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) } 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 705751f..8d4fe6c 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 @@ -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( } } } - - 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/resumeDetails/EditResumeScreen.kt new file mode 100644 index 0000000..b6fbe20 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeScreen.kt @@ -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 = "Пересчитать" + ) +} 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/resumeDetails/EditResumeViewModel.kt new file mode 100644 index 0000000..d2ff7d1 --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/EditResumeViewModel.kt @@ -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) } + } + } + } +} 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 9488b8e..84b80cb 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 @@ -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 = {