feat: add reasume validation, and update study grade enum

This commit is contained in:
MaximOksiuta
2025-11-22 17:25:04 +03:00
parent 385671e603
commit 4d9330159a
5 changed files with 162 additions and 60 deletions
@@ -125,31 +125,39 @@ data class EducationDTO(
@Serializable @Serializable
enum class EducationGradesDTO { enum class EducationGradesDTO {
@SerialName("common") @SerialName("basic_general_education")
Common, BasicGeneralEducation,
@SerialName("middle") @SerialName("secondary_general_education")
Middle, SecondaryGeneralEducation,
@SerialName("middle_spec") @SerialName("secondary_professional_education")
MiddleSpec, SecondaryProfessionalEducation,
@SerialName("high_not_finished") @SerialName("bachelor")
HighNotFinished, Bachelor,
@SerialName("high") @SerialName("specialist")
High, Specialist,
@SerialName("additional") @SerialName("master")
Additional; Master,
@SerialName("postgraduate_studies")
PostgraduateStudies,
@SerialName("other")
Other;
fun mapToDomain(): EducationGrades = when (this) { fun mapToDomain(): EducationGrades = when (this) {
Common -> EducationGrades.Common BasicGeneralEducation -> EducationGrades.BasicGeneralEducation
Middle -> EducationGrades.Middle SecondaryGeneralEducation -> EducationGrades.SecondaryGeneralEducation
MiddleSpec -> EducationGrades.MiddleSpec SecondaryProfessionalEducation -> EducationGrades.SecondaryProfessionalEducation
HighNotFinished -> EducationGrades.HighNotFinished Bachelor -> EducationGrades.Bachelor
High -> EducationGrades.High Specialist -> EducationGrades.Specialist
Additional -> EducationGrades.Additional Master -> EducationGrades.Master
PostgraduateStudies -> EducationGrades.PostgraduateStudies
Other -> EducationGrades.Other
} }
} }
@@ -39,12 +39,14 @@ data class Education(
) )
enum class EducationGrades { enum class EducationGrades {
Common, BasicGeneralEducation,
Middle, SecondaryGeneralEducation,
MiddleSpec, SecondaryProfessionalEducation,
HighNotFinished, Bachelor,
High, Specialist,
Additional Master,
PostgraduateStudies,
Other
} }
data class Project( data class Project(
@@ -1,10 +1,14 @@
package com.prodhack.moscow2025.domain.usecase.auth package com.prodhack.moscow2025.domain.usecase.auth
import android.util.Log
import android.util.Patterns import android.util.Patterns
import com.prodhack.moscow2025.domain.models.AuthField import com.prodhack.moscow2025.domain.models.AuthField
import com.prodhack.moscow2025.domain.models.ExperienceType import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.PhoneNumberPattern import com.prodhack.moscow2025.domain.models.PhoneNumberPattern
import com.prodhack.moscow2025.domain.models.Project
import com.prodhack.moscow2025.domain.models.ResumeField import com.prodhack.moscow2025.domain.models.ResumeField
import com.prodhack.moscow2025.domain.models.WorkExperience
import com.prodhack.moscow2025.presentation.screens.createResume.UIEducation
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
data class ValidationResult<T>( data class ValidationResult<T>(
@@ -87,7 +91,11 @@ class ValidateFieldsUseCase {
about: String, about: String,
position: String, position: String,
experience: ExperienceType?, experience: ExperienceType?,
keySkills: List<String> keySkills: List<String>,
city: String,
workExperience: List<WorkExperience>,
education: List<UIEducation>,
projects: List<Project>
): ValidationResult<ResumeField> { ): ValidationResult<ResumeField> {
val errors = buildMap { val errors = buildMap {
if (about.isBlank()) put(ResumeField.About, "Без этого мы не сможем рассчитать вашу ЗП") if (about.isBlank()) put(ResumeField.About, "Без этого мы не сможем рассчитать вашу ЗП")
@@ -101,6 +109,47 @@ class ValidateFieldsUseCase {
) )
if (keySkills.isEmpty()) put(ResumeField.KeySkills, "Укажите хотя бы один навык") if (keySkills.isEmpty()) put(ResumeField.KeySkills, "Укажите хотя бы один навык")
if (city.isEmpty()) put(ResumeField.City, "Без этого мы не сможем рассчитать вашу ЗП")
workExperience.forEachIndexed { index, exp ->
if (exp.place.isBlank()) put(
ResumeField.WorkExperiencePlace(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
if (exp.description.isBlank()) put(
ResumeField.WorkExperienceDescription(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
if (exp.monthDuration == null) put(
ResumeField.WorkExperienceMonthDuration(index),
"Введите корректное число"
)
}
education.forEachIndexed { index, educ ->
if (educ.place.isBlank()) put(
ResumeField.EducationPlace(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
if (educ.description.isBlank()) put(
ResumeField.EducationDescription(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
if (educ.specialization.isBlank()) put(
ResumeField.EducationSpecialization(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
}
projects.forEachIndexed { index, prj ->
if (prj.name.isBlank()) put(
ResumeField.ProjectName(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
if (prj.description.isBlank()) put(
ResumeField.ProjectDescription(index),
"Без этого мы не сможем рассчитать вашу ЗП"
)
}
} }
return ValidationResult(errors) return ValidationResult(errors)
} }
@@ -85,12 +85,16 @@ fun CreateResumeScreen(
label = "Какая должность вас интересует?", label = "Какая должность вас интересует?",
error = formState.value.errors[ResumeField.Position] error = formState.value.errors[ResumeField.Position]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = formState.value.city, value = formState.value.city,
onValueChange = viewModel::onCityChange, onValueChange = viewModel::onCityChange,
label = "Ваш город", label = "Ваш город",
error = formState.value.errors[ResumeField.City] error = formState.value.errors[ResumeField.City]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = formState.value.about, value = formState.value.about,
onValueChange = viewModel::onAboutChange, onValueChange = viewModel::onAboutChange,
@@ -99,6 +103,8 @@ fun CreateResumeScreen(
label = "Расскажите о себе", label = "Расскажите о себе",
error = formState.value.errors[ResumeField.Position] error = formState.value.errors[ResumeField.Position]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextFieldWithDropdown( TTTextFieldWithDropdown(
value = formState.value.experience?.friendlyName ?: "", value = formState.value.experience?.friendlyName ?: "",
onValueChange = {}, onValueChange = {},
@@ -112,6 +118,8 @@ fun CreateResumeScreen(
}, },
onDropdownItemSelected = viewModel::onExperienceSelect onDropdownItemSelected = viewModel::onExperienceSelect
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextFieldWithSearch( TTTextFieldWithSearch(
value = viewModel.skillSearchQuery.value, value = viewModel.skillSearchQuery.value,
onValueChange = { onValueChange = {
@@ -121,7 +129,7 @@ fun CreateResumeScreen(
error = formState.value.errors[ResumeField.Experience], error = formState.value.errors[ResumeField.Experience],
dropdownItems = viewModel.suggestedSkills.collectAsState(emptyList()).value, dropdownItems = viewModel.suggestedSkills.collectAsState(emptyList()).value,
dropDownItem = { dropDownItem = {
Text(text = it, style = typography.titleMedium, fontSize = 16.sp) Text(text = it, style = typography.labelLarge, fontSize = 16.sp)
}, },
onDropdownItemSelected = viewModel::onAddSkill, onDropdownItemSelected = viewModel::onAddSkill,
trailingIcon = { trailingIcon = {
@@ -151,6 +159,8 @@ fun CreateResumeScreen(
} }
} }
) )
Spacer(modifier = Modifier.height(Paddings.medium))
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy( horizontalArrangement = Arrangement.spacedBy(
@@ -167,7 +177,7 @@ fun CreateResumeScreen(
} }
} }
Spacer(modifier = Modifier.height(Paddings.large * 2)) Spacer(modifier = Modifier.height(Paddings.large))
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -195,7 +205,7 @@ fun CreateResumeScreen(
label = "Место работы", label = "Место работы",
error = formState.value.errors[ResumeField.WorkExperiencePlace(index)] error = formState.value.errors[ResumeField.WorkExperiencePlace(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = workExp.description, value = workExp.description,
onValueChange = { onValueChange = {
@@ -206,7 +216,7 @@ fun CreateResumeScreen(
label = "Расскажите подробнее", label = "Расскажите подробнее",
error = formState.value.errors[ResumeField.WorkExperienceDescription(index)] error = formState.value.errors[ResumeField.WorkExperienceDescription(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = workExp.monthDuration?.toString() ?: "", value = workExp.monthDuration?.toString() ?: "",
onValueChange = { onValueChange = {
@@ -215,6 +225,7 @@ fun CreateResumeScreen(
label = "Продолжительность (в месяцах)", label = "Продолжительность (в месяцах)",
error = formState.value.errors[ResumeField.WorkExperienceMonthDuration(index)] error = formState.value.errors[ResumeField.WorkExperienceMonthDuration(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
} }
if (formState.value.workExperience.isEmpty()) { if (formState.value.workExperience.isEmpty()) {
@@ -250,7 +261,7 @@ fun CreateResumeScreen(
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = "Образование:", text = "Ваше образование:",
style = typography.titleMedium, style = typography.titleMedium,
fontSize = 20.sp, fontSize = 20.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -272,7 +283,7 @@ fun CreateResumeScreen(
label = "Учебное заведение", label = "Учебное заведение",
error = formState.value.errors[ResumeField.EducationPlace(index)] error = formState.value.errors[ResumeField.EducationPlace(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextFieldWithDropdown( TTTextFieldWithDropdown(
value = education.grade.friendlyName, value = education.grade.friendlyName,
onValueChange = {}, onValueChange = {},
@@ -282,26 +293,31 @@ fun CreateResumeScreen(
error = formState.value.errors[ResumeField.EducationGrade(index)], error = formState.value.errors[ResumeField.EducationGrade(index)],
dropdownItems = viewModel.educationGradeOptions, dropdownItems = viewModel.educationGradeOptions,
dropDownItem = { dropDownItem = {
Text(text = it.friendlyName, style = typography.titleMedium, fontSize = 16.sp) Text(
text = it.friendlyName,
style = typography.labelLarge,
fontSize = 16.sp
)
}, },
onDropdownItemSelected = { viewModel.changeEducationGrade(index, it) } onDropdownItemSelected = { viewModel.changeEducationGrade(index, it) }
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = education.specialization, value = education.specialization,
onValueChange = { viewModel.changeEducationSpecialization(index, it) }, onValueChange = { viewModel.changeEducationSpecialization(index, it) },
label = "Специализация", label = "Специализация",
error = formState.value.errors[ResumeField.EducationSpecialization(index)] error = formState.value.errors[ResumeField.EducationSpecialization(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = education.description, value = education.description,
onValueChange = { viewModel.changeEducationDescription(index, it) }, onValueChange = { viewModel.changeEducationDescription(index, it) },
singleLine = false, singleLine = false,
maxLines = 10, maxLines = 10,
label = "Расскажите подробнее", label = "Расскажите подробнее (опционально)",
error = formState.value.errors[ResumeField.EducationDescription(index)] error = formState.value.errors[ResumeField.EducationDescription(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
} }
if (formState.value.education.isEmpty()) { if (formState.value.education.isEmpty()) {
@@ -338,7 +354,7 @@ fun CreateResumeScreen(
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = "Проекты:", text = "Интересные проекты:",
style = typography.titleMedium, style = typography.titleMedium,
fontSize = 20.sp, fontSize = 20.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -360,7 +376,7 @@ fun CreateResumeScreen(
label = "Название проекта", label = "Название проекта",
error = formState.value.errors[ResumeField.ProjectName(index)] error = formState.value.errors[ResumeField.ProjectName(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = project.description, value = project.description,
onValueChange = { viewModel.changeProjectDescription(index, it) }, onValueChange = { viewModel.changeProjectDescription(index, it) },
@@ -369,6 +385,7 @@ fun CreateResumeScreen(
label = "Расскажите подробнее", label = "Расскажите подробнее",
error = formState.value.errors[ResumeField.ProjectDescription(index)] error = formState.value.errors[ResumeField.ProjectDescription(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium))
} }
if (formState.value.projects.isEmpty()) { if (formState.value.projects.isEmpty()) {
@@ -407,6 +424,8 @@ fun CreateResumeScreen(
buttonText = "Узнать свою ЗП", buttonText = "Узнать свою ЗП",
isLoading = viewModel.resumeFillState.collectAsState().value.isLoading isLoading = viewModel.resumeFillState.collectAsState().value.isLoading
) )
Spacer(modifier = Modifier.height(Paddings.large))
} }
} }
} }
@@ -59,21 +59,40 @@ data class UIEducation(
val description: String val description: String
) )
//основное общее образование — basic_general_education
//
//среднее общее образование — secondary_general_education
//
//среднее профессиональное образование — secondary_professional_education
//
//бакалавриат — bachelor
//
//специалитет — specialist
//
//магистратура — master
//
//подготовка кадров высшей квалификации (аспірантура, ординатура, докторантура) — postgraduate_studies
sealed class UIEducationGrade(val friendlyName: String) { sealed class UIEducationGrade(val friendlyName: String) {
data object Common : UIEducationGrade("Общее") data object BasicGeneralEducation : UIEducationGrade("Общее")
data object Middle : UIEducationGrade("Среднее") data object SecondaryGeneralEducation : UIEducationGrade("Среднее")
data object MiddleSpec : UIEducationGrade("Средне-специальное") data object SecondaryProfessionalEducation : UIEducationGrade("Средне-специальное")
data object HighNotFinished : UIEducationGrade("Неоконченное высшее") data object Bachelor : UIEducationGrade("Бакалавриат")
data object High : UIEducationGrade("Высшее") data object Specialist : UIEducationGrade("Специалитет")
data object Additional : UIEducationGrade("Другое") data object Master : UIEducationGrade("Магистратура")
data object PostgraduateStudies: UIEducationGrade("Аспирантура и выше")
data object Other: UIEducationGrade("Другое")
fun mapToDomain(): EducationGrades = when (this) { fun mapToDomain(): EducationGrades = when (this) {
Common -> EducationGrades.Common BasicGeneralEducation -> EducationGrades.BasicGeneralEducation
Middle -> EducationGrades.Middle SecondaryGeneralEducation -> EducationGrades.SecondaryGeneralEducation
MiddleSpec -> EducationGrades.MiddleSpec SecondaryProfessionalEducation -> EducationGrades.SecondaryProfessionalEducation
HighNotFinished -> EducationGrades.HighNotFinished Bachelor -> EducationGrades.Bachelor
High -> EducationGrades.High Specialist -> EducationGrades.Specialist
Additional -> EducationGrades.Additional Master -> EducationGrades.Master
PostgraduateStudies -> EducationGrades.PostgraduateStudies
Other -> EducationGrades.Other
} }
} }
@@ -204,7 +223,8 @@ class CreateResumeViewModel(
experience.copy( experience.copy(
monthDuration = it monthDuration = it
) )
} ?: experience }
?: if (value.isEmpty()) experience.copy(monthDuration = null) else experience
} else experience } else experience
}, },
errors = it.errors errors = it.errors
@@ -229,12 +249,12 @@ class CreateResumeViewModel(
// Education // Education
val educationGradeOptions = listOf( val educationGradeOptions = listOf(
UIEducationGrade.Common, UIEducationGrade.BasicGeneralEducation,
UIEducationGrade.Middle, UIEducationGrade.SecondaryGeneralEducation,
UIEducationGrade.MiddleSpec, UIEducationGrade.SecondaryProfessionalEducation,
UIEducationGrade.HighNotFinished, UIEducationGrade.Bachelor,
UIEducationGrade.High, UIEducationGrade.Specialist,
UIEducationGrade.Additional UIEducationGrade.Master
) )
fun addNewEducation() { fun addNewEducation() {
@@ -242,7 +262,7 @@ class CreateResumeViewModel(
it.copy( it.copy(
education = it.education + UIEducation( education = it.education + UIEducation(
place = "", place = "",
grade = UIEducationGrade.High, grade = UIEducationGrade.Specialist,
specialization = "", specialization = "",
description = "" description = ""
) )
@@ -332,6 +352,10 @@ class CreateResumeViewModel(
position = _formStateFillResume.value.position, position = _formStateFillResume.value.position,
experience = _formStateFillResume.value.experience?.mapToDomain(), experience = _formStateFillResume.value.experience?.mapToDomain(),
keySkills = _formStateFillResume.value.keySkills.toList(), keySkills = _formStateFillResume.value.keySkills.toList(),
city = _formStateFillResume.value.city,
workExperience = _formStateFillResume.value.workExperience,
education = _formStateFillResume.value.education,
projects = _formStateFillResume.value.projects
) )
if (!validation.isValid) { if (!validation.isValid) {