You've already forked RekomenciMobile
feat: show details
This commit is contained in:
+4
@@ -6,6 +6,7 @@ import androidx.room.Query
|
|||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import com.prodhack.moscow2025.data.base.BasePaginationDAO
|
import com.prodhack.moscow2025.data.base.BasePaginationDAO
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
||||||
@@ -18,4 +19,7 @@ interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
|||||||
|
|
||||||
@Query("SELECT * FROM resumes")
|
@Query("SELECT * FROM resumes")
|
||||||
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
|
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM resumes WHERE id = :resumeId LIMIT 1")
|
||||||
|
fun getById(resumeId: String): Flow<ResumeEntity?>
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,10 @@ data class ResumeEntity(
|
|||||||
about = aboutMe,
|
about = aboutMe,
|
||||||
experienceType = ExperienceType.valueOf(experienceType),
|
experienceType = ExperienceType.valueOf(experienceType),
|
||||||
skills = keySkills.split("|"),
|
skills = keySkills.split("|"),
|
||||||
prediction = Pair(fromSalary, toSalary),
|
prediction = if (fromSalary == null && toSalary == null) null else Pair(
|
||||||
|
fromSalary,
|
||||||
|
toSalary
|
||||||
|
),
|
||||||
recommendedSkills = recommendedSkills.split("|"),
|
recommendedSkills = recommendedSkills.split("|"),
|
||||||
city = city,
|
city = city,
|
||||||
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
||||||
|
|||||||
+27
-1
@@ -5,6 +5,7 @@ import com.prodhack.moscow2025.data.base.BaseRepository
|
|||||||
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
|
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
|
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
|
import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
|
||||||
|
import com.prodhack.moscow2025.data.dto.ResumeDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeIdDTO
|
import com.prodhack.moscow2025.data.dto.ResumeIdDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeListDTO
|
import com.prodhack.moscow2025.data.dto.ResumeListDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
|
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
|
||||||
@@ -13,6 +14,7 @@ import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
|||||||
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||||
|
import com.prodhack.moscow2025.domain.utils.NetworkError
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
@@ -21,6 +23,9 @@ import io.ktor.http.contentType
|
|||||||
import io.ktor.http.parameters
|
import io.ktor.http.parameters
|
||||||
import io.ktor.http.path
|
import io.ktor.http.path
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
@@ -68,4 +73,25 @@ class ResumeRepositoryImpl(
|
|||||||
setBody(resumeForm.mapToData())
|
setBody(resumeForm.mapToData())
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
}.map { it.resumeId }
|
}.map { it.resumeId }
|
||||||
}
|
|
||||||
|
override fun getResume(resumeId: String): Flow<Result<ResumeModel>> =
|
||||||
|
merge(
|
||||||
|
resumeDao.getById(resumeId = resumeId).map { entity ->
|
||||||
|
entity?.let { Result.success(it.mapToDomain()) }
|
||||||
|
?: Result.failure(NetworkError.InputError("Резюме не найдено"))
|
||||||
|
},
|
||||||
|
flow {
|
||||||
|
emit(networkRequest<ResumeDTO> {
|
||||||
|
method = HttpMethod.Get
|
||||||
|
url {
|
||||||
|
path("resume", resumeId)
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
it.also {
|
||||||
|
resumeDao.upsertAll(listOf(it.mapToDB()))
|
||||||
|
}.mapToDomain()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+3
@@ -3,10 +3,13 @@ package com.prodhack.moscow2025.domain.interfaces.resumes
|
|||||||
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ResumeRepository {
|
interface ResumeRepository {
|
||||||
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
fun getResume(resumeId: String): Flow<Result<ResumeModel>>
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,14 +1,13 @@
|
|||||||
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.Education
|
||||||
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.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.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>(
|
||||||
@@ -94,7 +93,7 @@ class ValidateFieldsUseCase {
|
|||||||
keySkills: List<String>,
|
keySkills: List<String>,
|
||||||
city: String,
|
city: String,
|
||||||
workExperience: List<WorkExperience>,
|
workExperience: List<WorkExperience>,
|
||||||
education: List<UIEducation>,
|
education: List<Education>,
|
||||||
projects: List<Project>
|
projects: List<Project>
|
||||||
): ValidationResult<ResumeField> {
|
): ValidationResult<ResumeField> {
|
||||||
val errors = buildMap {
|
val errors = buildMap {
|
||||||
|
|||||||
+6
-4
@@ -1,12 +1,14 @@
|
|||||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class GetResumeInfoUseCase {
|
class GetResumeInfoUseCase(
|
||||||
operator fun invoke(): Flow<Result<ResumeModel>> {
|
private val resumeRepository: ResumeRepository
|
||||||
TODO()
|
) {
|
||||||
}
|
operator fun invoke(resumeId: String): Flow<Result<ResumeModel>> =
|
||||||
|
resumeRepository.getResume(resumeId)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,7 @@
|
|||||||
package com.prodhack.moscow2025.presentation.dataModels
|
package com.prodhack.moscow2025.presentation.dataModels
|
||||||
|
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
|
||||||
|
|
||||||
data class UIResumeBaseInfo(
|
data class UIResumeBaseInfo(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -11,7 +12,5 @@ data class UIResumeBaseInfo(
|
|||||||
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
|
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
|
||||||
id = id,
|
id = id,
|
||||||
positionName = position,
|
positionName = position,
|
||||||
salary = prediction?.first?.let { from ->
|
salary = prediction.toSalaryRangeString()
|
||||||
prediction.second?.let { to -> "$from-$to₽" } ?: "$from₽"
|
|
||||||
} ?: prediction?.second?.let { "$it₽" } ?: "Загрузка..."
|
|
||||||
)
|
)
|
||||||
+252
-275
@@ -2,6 +2,7 @@ package com.prodhack.moscow2025.presentation.screens.createResume
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -29,6 +30,10 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
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 com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
|
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeField
|
import com.prodhack.moscow2025.domain.models.ResumeField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
@@ -37,6 +42,7 @@ import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithD
|
|||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ fun CreateResumeScreen(
|
|||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
TTTextFieldWithDropdown(
|
TTTextFieldWithDropdown(
|
||||||
value = formState.value.experience?.friendlyName ?: "",
|
value = formState.value.experience?.toReadableText() ?: "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
maxLines = Int.MAX_VALUE,
|
maxLines = Int.MAX_VALUE,
|
||||||
@@ -116,7 +122,7 @@ fun CreateResumeScreen(
|
|||||||
error = formState.value.errors[ResumeField.Experience],
|
error = formState.value.errors[ResumeField.Experience],
|
||||||
dropdownItems = viewModel.experienceOptions,
|
dropdownItems = viewModel.experienceOptions,
|
||||||
dropDownItem = {
|
dropDownItem = {
|
||||||
Text(text = it.friendlyName, style = typography.labelLarge, fontSize = 16.sp)
|
Text(text = it.toReadableText(), style = typography.labelLarge, fontSize = 16.sp)
|
||||||
},
|
},
|
||||||
onDropdownItemSelected = viewModel::onExperienceSelect
|
onDropdownItemSelected = viewModel::onExperienceSelect
|
||||||
)
|
)
|
||||||
@@ -181,317 +187,90 @@ fun CreateResumeScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
Card {
|
SectionCard(title = "Подробнее о вашем опыте работы:") {
|
||||||
Column(modifier = Modifier.padding(Paddings.medium)) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Подробнее о вашем опыте работы:",
|
|
||||||
style = typography.titleMedium,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
formState.value.workExperience.forEachIndexed { index, workExp ->
|
formState.value.workExperience.forEachIndexed { index, workExp ->
|
||||||
Text(
|
WorkExperienceForm(
|
||||||
text = "№${index + 1}:",
|
index = index,
|
||||||
style = typography.labelLarge,
|
workExp = workExp,
|
||||||
fontSize = 18.sp
|
errors = formState.value.errors,
|
||||||
|
onPlaceChange = { viewModel.changeWorkExperiencePlace(index, it) },
|
||||||
|
onDescriptionChange = { viewModel.changeWorkExperienceDescription(index, it) },
|
||||||
|
onDurationChange = { viewModel.changeWorkExperienceMonthDuration(index, it) },
|
||||||
|
onRemove = { viewModel.removeExperience(index) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
TTTextField(
|
|
||||||
value = workExp.place,
|
|
||||||
onValueChange = {
|
|
||||||
viewModel.changeWorkExperiencePlace(index, it)
|
|
||||||
},
|
|
||||||
label = "Место работы",
|
|
||||||
error = formState.value.errors[ResumeField.WorkExperiencePlace(index)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
TTTextField(
|
|
||||||
value = workExp.description,
|
|
||||||
onValueChange = {
|
|
||||||
viewModel.changeWorkExperienceDescription(index, it)
|
|
||||||
},
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = 10,
|
|
||||||
label = "Расскажите подробнее",
|
|
||||||
error = formState.value.errors[ResumeField.WorkExperienceDescription(
|
|
||||||
index
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
TTTextField(
|
|
||||||
value = workExp.monthDuration?.toString() ?: "",
|
|
||||||
onValueChange = {
|
|
||||||
viewModel.changeWorkExperienceMonthDuration(index, it)
|
|
||||||
},
|
|
||||||
label = "Продолжительность (в месяцах)",
|
|
||||||
error = formState.value.errors[ResumeField.WorkExperienceMonthDuration(
|
|
||||||
index
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = { viewModel.removeExperience(index) },
|
|
||||||
colors = ButtonColors(
|
|
||||||
containerColor = colorScheme.errorContainer,
|
|
||||||
contentColor = colorScheme.onErrorContainer,
|
|
||||||
disabledContainerColor = colorScheme.errorContainer,
|
|
||||||
disabledContentColor = colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Удалить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formState.value.workExperience.isEmpty()) {
|
if (formState.value.workExperience.isEmpty()) {
|
||||||
Text(
|
EmptyStateText()
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Пока ничего нет",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
AddItemButton(
|
||||||
modifier = Modifier
|
text = "Добавить",
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = viewModel::addNewExperience,
|
onClick = viewModel::addNewExperience,
|
||||||
colors = ButtonColors(
|
containerColor = colorScheme.onSecondary,
|
||||||
containerColor = colorScheme.onSecondary,
|
contentColor = colorScheme.secondary
|
||||||
contentColor = colorScheme.secondary,
|
)
|
||||||
disabledContainerColor = colorScheme.onSecondary,
|
|
||||||
disabledContentColor = colorScheme.secondary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Добавить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
Card {
|
SectionCard(title = "Ваше образование:") {
|
||||||
Column(modifier = Modifier.padding(Paddings.medium)) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Ваше образование:",
|
|
||||||
style = typography.titleMedium,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
|
||||||
|
|
||||||
formState.value.education.forEachIndexed { index, education ->
|
formState.value.education.forEachIndexed { index, education ->
|
||||||
Text(
|
EducationForm(
|
||||||
text = "№${index + 1}:",
|
index = index,
|
||||||
style = typography.labelLarge,
|
education = education,
|
||||||
fontSize = 18.sp
|
errors = formState.value.errors,
|
||||||
|
grades = viewModel.educationGradeOptions,
|
||||||
|
onPlaceChange = { viewModel.changeEducationPlace(index, it) },
|
||||||
|
onGradeChange = { viewModel.changeEducationGrade(index, it) },
|
||||||
|
onSpecializationChange = { viewModel.changeEducationSpecialization(index, it) },
|
||||||
|
onDescriptionChange = { viewModel.changeEducationDescription(index, it) },
|
||||||
|
onRemove = { viewModel.removeEducation(index) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
TTTextField(
|
|
||||||
value = education.place,
|
|
||||||
onValueChange = { viewModel.changeEducationPlace(index, it) },
|
|
||||||
label = "Учебное заведение",
|
|
||||||
error = formState.value.errors[ResumeField.EducationPlace(index)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
TTTextFieldWithDropdown(
|
|
||||||
value = education.grade.friendlyName,
|
|
||||||
onValueChange = {},
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = Int.MAX_VALUE,
|
|
||||||
label = "Уровень образования",
|
|
||||||
error = formState.value.errors[ResumeField.EducationGrade(index)],
|
|
||||||
dropdownItems = viewModel.educationGradeOptions,
|
|
||||||
dropDownItem = {
|
|
||||||
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 = "Расскажите подробнее (опционально)",
|
|
||||||
error = formState.value.errors[ResumeField.EducationDescription(index)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = { viewModel.removeEducation(index) },
|
|
||||||
colors = ButtonColors(
|
|
||||||
containerColor = colorScheme.errorContainer,
|
|
||||||
contentColor = colorScheme.onErrorContainer,
|
|
||||||
disabledContainerColor = colorScheme.errorContainer,
|
|
||||||
disabledContentColor = colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Удалить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formState.value.education.isEmpty()) {
|
if (formState.value.education.isEmpty()) {
|
||||||
Text(
|
EmptyStateText()
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Пока ничего нет",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
AddItemButton(
|
||||||
modifier = Modifier
|
text = "Добавить",
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = viewModel::addNewEducation,
|
onClick = viewModel::addNewEducation,
|
||||||
colors = ButtonColors(
|
containerColor = colorScheme.onSecondary,
|
||||||
containerColor = colorScheme.onSecondary,
|
contentColor = colorScheme.secondary
|
||||||
contentColor = colorScheme.secondary,
|
)
|
||||||
disabledContainerColor = colorScheme.onSecondary,
|
|
||||||
disabledContentColor = colorScheme.secondary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Добавить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
Card {
|
SectionCard(title = "Интересные проекты:") {
|
||||||
Column(modifier = Modifier.padding(Paddings.medium)) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Интересные проекты:",
|
|
||||||
style = typography.titleMedium,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
|
||||||
|
|
||||||
formState.value.projects.forEachIndexed { index, project ->
|
formState.value.projects.forEachIndexed { index, project ->
|
||||||
Text(
|
ProjectForm(
|
||||||
text = "№${index + 1}:",
|
index = index,
|
||||||
style = typography.labelLarge,
|
project = project,
|
||||||
fontSize = 18.sp
|
errors = formState.value.errors,
|
||||||
|
onNameChange = { viewModel.changeProjectName(index, it) },
|
||||||
|
onDescriptionChange = { viewModel.changeProjectDescription(index, it) },
|
||||||
|
onRemove = { viewModel.removeProject(index) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
TTTextField(
|
|
||||||
value = project.name,
|
|
||||||
onValueChange = { viewModel.changeProjectName(index, it) },
|
|
||||||
label = "Название проекта",
|
|
||||||
error = formState.value.errors[ResumeField.ProjectName(index)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
TTTextField(
|
|
||||||
value = project.description,
|
|
||||||
onValueChange = { viewModel.changeProjectDescription(index, it) },
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = 10,
|
|
||||||
label = "Расскажите подробнее",
|
|
||||||
error = formState.value.errors[ResumeField.ProjectDescription(index)]
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = { viewModel.removeProject(index) },
|
|
||||||
colors = ButtonColors(
|
|
||||||
containerColor = colorScheme.errorContainer,
|
|
||||||
contentColor = colorScheme.onErrorContainer,
|
|
||||||
disabledContainerColor = colorScheme.errorContainer,
|
|
||||||
disabledContentColor = colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Удалить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formState.value.projects.isEmpty()) {
|
if (formState.value.projects.isEmpty()) {
|
||||||
Text(
|
EmptyStateText()
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
text = "Пока ничего нет",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
AddItemButton(
|
||||||
modifier = Modifier
|
text = "Добавить",
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = Shapes.smallRoundedBox,
|
|
||||||
onClick = viewModel::addNewProject,
|
onClick = viewModel::addNewProject,
|
||||||
colors = ButtonColors(
|
containerColor = colorScheme.onSecondary,
|
||||||
containerColor = colorScheme.onSecondary,
|
contentColor = colorScheme.secondary
|
||||||
contentColor = colorScheme.secondary,
|
)
|
||||||
disabledContainerColor = colorScheme.onSecondary,
|
|
||||||
disabledContentColor = colorScheme.secondary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Добавить",
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
BigButton(
|
BigButton(
|
||||||
@@ -504,3 +283,201 @@ fun CreateResumeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionCard(
|
||||||
|
title: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Card {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(Paddings.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.medium)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = title,
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WorkExperienceForm(
|
||||||
|
index: Int,
|
||||||
|
workExp: WorkExperience,
|
||||||
|
errors: Map<ResumeField, String>,
|
||||||
|
onPlaceChange: (String) -> Unit,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onDurationChange: (String) -> Unit,
|
||||||
|
onRemove: () -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Text(text = "№${index + 1}:", style = typography.labelLarge, fontSize = 18.sp)
|
||||||
|
TTTextField(
|
||||||
|
value = workExp.place,
|
||||||
|
onValueChange = onPlaceChange,
|
||||||
|
label = "Место работы",
|
||||||
|
error = errors[ResumeField.WorkExperiencePlace(index)]
|
||||||
|
)
|
||||||
|
TTTextField(
|
||||||
|
value = workExp.description,
|
||||||
|
onValueChange = onDescriptionChange,
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 10,
|
||||||
|
label = "Расскажите подробнее",
|
||||||
|
error = errors[ResumeField.WorkExperienceDescription(index)]
|
||||||
|
)
|
||||||
|
TTTextField(
|
||||||
|
value = workExp.monthDuration?.toString() ?: "",
|
||||||
|
onValueChange = onDurationChange,
|
||||||
|
label = "Продолжительность (в месяцах)",
|
||||||
|
error = errors[ResumeField.WorkExperienceMonthDuration(index)]
|
||||||
|
)
|
||||||
|
DeleteItemButton(onClick = onRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EducationForm(
|
||||||
|
index: Int,
|
||||||
|
education: Education,
|
||||||
|
errors: Map<ResumeField, String>,
|
||||||
|
grades: List<EducationGrades>,
|
||||||
|
onPlaceChange: (String) -> Unit,
|
||||||
|
onGradeChange: (EducationGrades) -> Unit,
|
||||||
|
onSpecializationChange: (String) -> Unit,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onRemove: () -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Text(text = "№${index + 1}:", style = typography.labelLarge, fontSize = 18.sp)
|
||||||
|
TTTextField(
|
||||||
|
value = education.place,
|
||||||
|
onValueChange = onPlaceChange,
|
||||||
|
label = "Учебное заведение",
|
||||||
|
error = errors[ResumeField.EducationPlace(index)]
|
||||||
|
)
|
||||||
|
TTTextFieldWithDropdown(
|
||||||
|
value = education.grade.toReadableText(),
|
||||||
|
onValueChange = {},
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
label = "Уровень образования",
|
||||||
|
error = errors[ResumeField.EducationGrade(index)],
|
||||||
|
dropdownItems = grades,
|
||||||
|
dropDownItem = {
|
||||||
|
Text(
|
||||||
|
text = it.toReadableText(),
|
||||||
|
style = typography.labelLarge,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDropdownItemSelected = onGradeChange
|
||||||
|
)
|
||||||
|
TTTextField(
|
||||||
|
value = education.specialization,
|
||||||
|
onValueChange = onSpecializationChange,
|
||||||
|
label = "Специализация",
|
||||||
|
error = errors[ResumeField.EducationSpecialization(index)]
|
||||||
|
)
|
||||||
|
TTTextField(
|
||||||
|
value = education.description,
|
||||||
|
onValueChange = onDescriptionChange,
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 10,
|
||||||
|
label = "Расскажите подробнее (опционально)",
|
||||||
|
error = errors[ResumeField.EducationDescription(index)]
|
||||||
|
)
|
||||||
|
DeleteItemButton(onClick = onRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProjectForm(
|
||||||
|
index: Int,
|
||||||
|
project: Project,
|
||||||
|
errors: Map<ResumeField, String>,
|
||||||
|
onNameChange: (String) -> Unit,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onRemove: () -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Text(text = "№${index + 1}:", style = typography.labelLarge, fontSize = 18.sp)
|
||||||
|
TTTextField(
|
||||||
|
value = project.name,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
label = "Название проекта",
|
||||||
|
error = errors[ResumeField.ProjectName(index)]
|
||||||
|
)
|
||||||
|
TTTextField(
|
||||||
|
value = project.description,
|
||||||
|
onValueChange = onDescriptionChange,
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 10,
|
||||||
|
label = "Расскажите подробнее",
|
||||||
|
error = errors[ResumeField.ProjectDescription(index)]
|
||||||
|
)
|
||||||
|
DeleteItemButton(onClick = onRemove)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AddItemButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
containerColor: androidx.compose.ui.graphics.Color,
|
||||||
|
contentColor: androidx.compose.ui.graphics.Color
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = onClick,
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
disabledContainerColor = containerColor,
|
||||||
|
disabledContentColor = contentColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteItemButton(onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = Shapes.smallRoundedBox,
|
||||||
|
onClick = onClick,
|
||||||
|
colors = ButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Удалить",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyStateText() {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = "Пока ничего нет",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+11
-93
@@ -21,81 +21,19 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import kotlin.collections.minus
|
|
||||||
|
|
||||||
data class ResumeFormState(
|
data class ResumeFormState(
|
||||||
val about: String = "",
|
val about: String = "",
|
||||||
val position: String = "",
|
val position: String = "",
|
||||||
val experience: UIExperienceCount? = null,
|
val experience: ExperienceType? = null,
|
||||||
val keySkills: Set<String> = emptySet(),
|
val keySkills: Set<String> = emptySet(),
|
||||||
val city: String = "",
|
val city: String = "",
|
||||||
val workExperience: List<WorkExperience> = emptyList(),
|
val workExperience: List<WorkExperience> = emptyList(),
|
||||||
val education: List<UIEducation> = emptyList(),
|
val education: List<Education> = emptyList(),
|
||||||
val projects: List<Project> = emptyList(),
|
val projects: List<Project> = emptyList(),
|
||||||
val errors: Map<ResumeField, String> = emptyMap()
|
val errors: Map<ResumeField, String> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class UIExperienceCount(val friendlyName: String) {
|
|
||||||
data object NoExperience : UIExperienceCount("Без опыта")
|
|
||||||
data object LessThan1 : UIExperienceCount("Меньше года")
|
|
||||||
data object Between1And3 : UIExperienceCount("От 1 до 3 лет")
|
|
||||||
data object Between3And6 : UIExperienceCount("От 3 до 6 лет")
|
|
||||||
data object MoreThan6 : UIExperienceCount("Более 6 лет")
|
|
||||||
|
|
||||||
fun mapToDomain(): ExperienceType =
|
|
||||||
when (this) {
|
|
||||||
is NoExperience -> ExperienceType.NoExperience
|
|
||||||
is LessThan1 -> ExperienceType.LessThan1
|
|
||||||
is Between1And3 -> ExperienceType.Between1And3
|
|
||||||
is Between3And6 -> ExperienceType.Between3And6
|
|
||||||
is MoreThan6 -> ExperienceType.MoreThan6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UIEducation(
|
|
||||||
val place: String,
|
|
||||||
val grade: UIEducationGrade,
|
|
||||||
val specialization: String,
|
|
||||||
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 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) {
|
|
||||||
BasicGeneralEducation -> EducationGrades.BasicGeneralEducation
|
|
||||||
SecondaryGeneralEducation -> EducationGrades.SecondaryGeneralEducation
|
|
||||||
SecondaryProfessionalEducation -> EducationGrades.SecondaryProfessionalEducation
|
|
||||||
Bachelor -> EducationGrades.Bachelor
|
|
||||||
Specialist -> EducationGrades.Specialist
|
|
||||||
Master -> EducationGrades.Master
|
|
||||||
PostgraduateStudies -> EducationGrades.PostgraduateStudies
|
|
||||||
Other -> EducationGrades.Other
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class CreateResumeViewModel(
|
class CreateResumeViewModel(
|
||||||
private val suggestSkillsUseCase: SuggestSkillsUseCase,
|
private val suggestSkillsUseCase: SuggestSkillsUseCase,
|
||||||
@@ -136,15 +74,9 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val experienceOptions = listOf(
|
val experienceOptions = ExperienceType.entries.toList()
|
||||||
UIExperienceCount.NoExperience,
|
|
||||||
UIExperienceCount.LessThan1,
|
|
||||||
UIExperienceCount.Between1And3,
|
|
||||||
UIExperienceCount.Between3And6,
|
|
||||||
UIExperienceCount.MoreThan6
|
|
||||||
)
|
|
||||||
|
|
||||||
fun onExperienceSelect(value: UIExperienceCount) {
|
fun onExperienceSelect(value: ExperienceType) {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
experience = value,
|
experience = value,
|
||||||
@@ -249,21 +181,14 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
val educationGradeOptions = listOf(
|
val educationGradeOptions = EducationGrades.entries.toList()
|
||||||
UIEducationGrade.BasicGeneralEducation,
|
|
||||||
UIEducationGrade.SecondaryGeneralEducation,
|
|
||||||
UIEducationGrade.SecondaryProfessionalEducation,
|
|
||||||
UIEducationGrade.Bachelor,
|
|
||||||
UIEducationGrade.Specialist,
|
|
||||||
UIEducationGrade.Master
|
|
||||||
)
|
|
||||||
|
|
||||||
fun addNewEducation() {
|
fun addNewEducation() {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
education = it.education + UIEducation(
|
education = it.education + Education(
|
||||||
place = "",
|
place = "",
|
||||||
grade = UIEducationGrade.Specialist,
|
grade = EducationGrades.Specialist,
|
||||||
specialization = "",
|
specialization = "",
|
||||||
description = ""
|
description = ""
|
||||||
)
|
)
|
||||||
@@ -295,7 +220,7 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeEducationGrade(index: Int, value: UIEducationGrade) {
|
fun changeEducationGrade(index: Int, value: EducationGrades) {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
education = it.education.mapIndexed { ind, education ->
|
education = it.education.mapIndexed { ind, education ->
|
||||||
@@ -375,7 +300,7 @@ class CreateResumeViewModel(
|
|||||||
val validation = validateDataUseCase.validateResume(
|
val validation = validateDataUseCase.validateResume(
|
||||||
about = _formStateFillResume.value.about,
|
about = _formStateFillResume.value.about,
|
||||||
position = _formStateFillResume.value.position,
|
position = _formStateFillResume.value.position,
|
||||||
experience = _formStateFillResume.value.experience?.mapToDomain(),
|
experience = _formStateFillResume.value.experience,
|
||||||
keySkills = _formStateFillResume.value.keySkills.toList(),
|
keySkills = _formStateFillResume.value.keySkills.toList(),
|
||||||
city = _formStateFillResume.value.city,
|
city = _formStateFillResume.value.city,
|
||||||
workExperience = _formStateFillResume.value.workExperience,
|
workExperience = _formStateFillResume.value.workExperience,
|
||||||
@@ -396,17 +321,10 @@ class CreateResumeViewModel(
|
|||||||
position = position,
|
position = position,
|
||||||
about = about,
|
about = about,
|
||||||
skills = keySkills.toList(),
|
skills = keySkills.toList(),
|
||||||
experienceType = experience!!.mapToDomain(),
|
experienceType = experience!!,
|
||||||
city = city,
|
city = city,
|
||||||
experience = workExperience,
|
experience = workExperience,
|
||||||
education = education.map {
|
education = education,
|
||||||
Education(
|
|
||||||
place = it.place,
|
|
||||||
grade = it.grade.mapToDomain(),
|
|
||||||
specialization = it.specialization,
|
|
||||||
description = it.description
|
|
||||||
)
|
|
||||||
},
|
|
||||||
projects = projects
|
projects = projects
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+351
-2
@@ -1,14 +1,58 @@
|
|||||||
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.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardColors
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||||
|
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 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 ResumeDetailsScreen(
|
fun ErrorCollectorScope.ResumeDetailsScreen(
|
||||||
navBackStackEntry: NavBackStackEntry,
|
navBackStackEntry: NavBackStackEntry,
|
||||||
viewModel: ResumeDetailsViewModel = koinViewModel {
|
viewModel: ResumeDetailsViewModel = koinViewModel {
|
||||||
parametersOf(
|
parametersOf(
|
||||||
@@ -16,5 +60,310 @@ fun ResumeDetailsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
}
|
val resumeState by viewModel.resumeState.collectAsStateWithCallbacks()
|
||||||
|
|
||||||
|
resumeState.FoldUIStateWithGlobalCallbacks(
|
||||||
|
onLoading = {
|
||||||
|
LoadingPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Paddings.large)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
ErrorPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Paddings.large)
|
||||||
|
) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { resume ->
|
||||||
|
ResumeDetailsContent(
|
||||||
|
resume = resume,
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onEdit = {
|
||||||
|
Toast.makeText(context, "Редактирование пока недоступно", Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
onHistory = {
|
||||||
|
Toast.makeText(context, "История появится позже", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ResumeDetailsContent(
|
||||||
|
resume: ResumeModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
onHistory: () -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = Paddings.large)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.rotate(180f)
|
||||||
|
.noRippleClickable(onBack),
|
||||||
|
painter = painterResource(R.drawable.ic_arr_details),
|
||||||
|
tint = colorScheme.onBackground,
|
||||||
|
contentDescription = "go back"
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Детали резюме",
|
||||||
|
style = typography.titleLarge,
|
||||||
|
fontSize = 22.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
SectionContainer {
|
||||||
|
Text(
|
||||||
|
"${resume.position} • ${resume.prediction.toSalaryRangeString()}",
|
||||||
|
style = typography.titleLarge,
|
||||||
|
fontSize = 28.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = resume.city,
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Опыт: ${resume.experienceType.toReadableText()}",
|
||||||
|
style = typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
SectionContainer(title = "О себе") {
|
||||||
|
Text(
|
||||||
|
text = resume.about.ifBlank { "Описание отсутствует" },
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Ключевые навыки") {
|
||||||
|
if (resume.skills.isEmpty()) {
|
||||||
|
Text("Навыки не указаны", style = typography.bodyMedium)
|
||||||
|
} else {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
resume.skills.forEach { skill ->
|
||||||
|
TBubble(text = skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resume.recommendedSkills
|
||||||
|
// ?.filter { it.isNotBlank() }
|
||||||
|
// ?.takeIf { it.isNotEmpty() }
|
||||||
|
// ?.let { skills ->
|
||||||
|
SectionContainer(
|
||||||
|
title = "Рекомендуем изучить",
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = colorScheme.primaryContainer,
|
||||||
|
contentColor = colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
listOf("skill1", "skill2").forEach {
|
||||||
|
TBubble(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Опыт работы") {
|
||||||
|
if (resume.experience.isEmpty()) {
|
||||||
|
Text("Опыт не указан", style = typography.bodyMedium)
|
||||||
|
} else {
|
||||||
|
resume.experience.forEachIndexed { index, work ->
|
||||||
|
WorkExperienceCard(index = index, workExperience = work)
|
||||||
|
if (index != resume.experience.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Образование") {
|
||||||
|
if (resume.education.isEmpty()) {
|
||||||
|
Text("Не указано", style = typography.bodyMedium)
|
||||||
|
} else {
|
||||||
|
resume.education.forEachIndexed { index, education ->
|
||||||
|
EducationCard(index = index, education = education)
|
||||||
|
if (index != resume.education.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Проекты") {
|
||||||
|
if (resume.projects.isEmpty()) {
|
||||||
|
Text("Проекты не указаны", style = typography.bodyMedium)
|
||||||
|
} else {
|
||||||
|
resume.projects.forEachIndexed { index, project ->
|
||||||
|
ProjectCard(index = index, project = project)
|
||||||
|
if (index != resume.projects.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String = "",
|
||||||
|
colors: CardColors = CardDefaults.cardColors(),
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = colors,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Paddings.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
if (title.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WorkExperienceCard(index: Int, workExperience: WorkExperience) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Место №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(workExperience.place, style = typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = workExperience.description,
|
||||||
|
style = typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Длительность: ${workExperience.monthDuration.toMonthText()}",
|
||||||
|
style = typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EducationCard(index: Int, education: Education) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Учебное место №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(education.place, style = typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = "Ступень: ${education.grade.toReadableText()}",
|
||||||
|
style = typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Специализация: ${education.specialization}",
|
||||||
|
style = typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = education.description,
|
||||||
|
style = typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProjectCard(index: Int, project: Project) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Проект №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(project.name, style = typography.titleMedium)
|
||||||
|
Text(project.description, style = typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int?.toMonthText(): String = when {
|
||||||
|
this == null -> "Не указано"
|
||||||
|
this < 12 -> "$this мес."
|
||||||
|
else -> {
|
||||||
|
val years = this / 12
|
||||||
|
val months = this % 12
|
||||||
|
if (months == 0) "$years г." else "$years г. $months мес."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-1
@@ -1,7 +1,10 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase
|
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Provided
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
@@ -10,4 +13,14 @@ class ResumeDetailsViewModel(
|
|||||||
@Provided resumeId: String,
|
@Provided resumeId: String,
|
||||||
private val getResumeInfoUseCase: GetResumeInfoUseCase
|
private val getResumeInfoUseCase: GetResumeInfoUseCase
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
}
|
private val _resumeState = MutableUIStateFlow<ResumeModel>()
|
||||||
|
val resumeState: StateFlow<UIState<ResumeModel>> = _resumeState
|
||||||
|
|
||||||
|
fun loadResume(resumeId: String) {
|
||||||
|
getResumeInfoUseCase(resumeId).collectRequest(_resumeState)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadResume(resumeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.utils
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
|
|
||||||
|
fun ExperienceType.toReadableText(): String = when (this) {
|
||||||
|
ExperienceType.NoExperience -> "Нет опыта"
|
||||||
|
ExperienceType.LessThan1 -> "Меньше года"
|
||||||
|
ExperienceType.Between1And3 -> "1-3 года"
|
||||||
|
ExperienceType.Between3And6 -> "3-6 лет"
|
||||||
|
ExperienceType.MoreThan6 -> "Более 6 лет"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EducationGrades.toReadableText(): String = when (this) {
|
||||||
|
EducationGrades.BasicGeneralEducation -> "Базовое общее"
|
||||||
|
EducationGrades.SecondaryGeneralEducation -> "Среднее общее"
|
||||||
|
EducationGrades.SecondaryProfessionalEducation -> "Среднее профессиональное"
|
||||||
|
EducationGrades.Bachelor -> "Бакалавр"
|
||||||
|
EducationGrades.Specialist -> "Специалист"
|
||||||
|
EducationGrades.Master -> "Магистр"
|
||||||
|
EducationGrades.PostgraduateStudies -> "Аспирантура"
|
||||||
|
EducationGrades.Other -> "Другое"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Pair<Int?, Int?>?.toSalaryRangeString(): String = when {
|
||||||
|
this == null -> "Загрузка..."
|
||||||
|
first != null && second != null -> "${first}₽ - ${second}₽"
|
||||||
|
first != null -> "от ${first}₽"
|
||||||
|
second != null -> "до ${second}₽"
|
||||||
|
else -> "Ошибка"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user