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