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 com.prodhack.moscow2025.data.base.BasePaginationDAO
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
||||
@@ -18,4 +19,7 @@ interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
||||
|
||||
@Query("SELECT * FROM resumes")
|
||||
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,
|
||||
experienceType = ExperienceType.valueOf(experienceType),
|
||||
skills = keySkills.split("|"),
|
||||
prediction = Pair(fromSalary, toSalary),
|
||||
prediction = if (fromSalary == null && toSalary == null) null else Pair(
|
||||
fromSalary,
|
||||
toSalary
|
||||
),
|
||||
recommendedSkills = recommendedSkills.split("|"),
|
||||
city = city,
|
||||
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
||||
|
||||
+26
@@ -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.local_db.AppDatabase
|
||||
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.ResumeListDTO
|
||||
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.ResumeModel
|
||||
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.url
|
||||
import io.ktor.http.ContentType
|
||||
@@ -21,6 +23,9 @@ import io.ktor.http.contentType
|
||||
import io.ktor.http.parameters
|
||||
import io.ktor.http.path
|
||||
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
|
||||
|
||||
@Single
|
||||
@@ -68,4 +73,25 @@ class ResumeRepositoryImpl(
|
||||
setBody(resumeForm.mapToData())
|
||||
contentType(ContentType.Application.Json)
|
||||
}.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.ResumeModel
|
||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ResumeRepository {
|
||||
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
||||
|
||||
suspend fun suggestSkills(query: String): Result<List<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
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
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.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>(
|
||||
@@ -94,7 +93,7 @@ class ValidateFieldsUseCase {
|
||||
keySkills: List<String>,
|
||||
city: String,
|
||||
workExperience: List<WorkExperience>,
|
||||
education: List<UIEducation>,
|
||||
education: List<Education>,
|
||||
projects: List<Project>
|
||||
): ValidationResult<ResumeField> {
|
||||
val errors = buildMap {
|
||||
|
||||
+6
-4
@@ -1,12 +1,14 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||
|
||||
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@Single
|
||||
class GetResumeInfoUseCase {
|
||||
operator fun invoke(): Flow<Result<ResumeModel>> {
|
||||
TODO()
|
||||
}
|
||||
class GetResumeInfoUseCase(
|
||||
private val resumeRepository: ResumeRepository
|
||||
) {
|
||||
operator fun invoke(resumeId: String): Flow<Result<ResumeModel>> =
|
||||
resumeRepository.getResume(resumeId)
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,6 +1,7 @@
|
||||
package com.prodhack.moscow2025.presentation.dataModels
|
||||
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
|
||||
|
||||
data class UIResumeBaseInfo(
|
||||
val id: String,
|
||||
@@ -11,7 +12,5 @@ data class UIResumeBaseInfo(
|
||||
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
|
||||
id = id,
|
||||
positionName = position,
|
||||
salary = prediction?.first?.let { from ->
|
||||
prediction.second?.let { to -> "$from-$to₽" } ?: "$from₽"
|
||||
} ?: prediction?.second?.let { "$it₽" } ?: "Загрузка..."
|
||||
salary = prediction.toSalaryRangeString()
|
||||
)
|
||||
+252
-275
@@ -2,6 +2,7 @@ package com.prodhack.moscow2025.presentation.screens.createResume
|
||||
|
||||
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
|
||||
@@ -29,6 +30,10 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.presentation.components.standart.BigButton
|
||||
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.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@@ -108,7 +114,7 @@ fun CreateResumeScreen(
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
TTTextFieldWithDropdown(
|
||||
value = formState.value.experience?.friendlyName ?: "",
|
||||
value = formState.value.experience?.toReadableText() ?: "",
|
||||
onValueChange = {},
|
||||
singleLine = false,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
@@ -116,7 +122,7 @@ fun CreateResumeScreen(
|
||||
error = formState.value.errors[ResumeField.Experience],
|
||||
dropdownItems = viewModel.experienceOptions,
|
||||
dropDownItem = {
|
||||
Text(text = it.friendlyName, style = typography.labelLarge, fontSize = 16.sp)
|
||||
Text(text = it.toReadableText(), style = typography.labelLarge, fontSize = 16.sp)
|
||||
},
|
||||
onDropdownItemSelected = viewModel::onExperienceSelect
|
||||
)
|
||||
@@ -181,317 +187,90 @@ fun CreateResumeScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
Card {
|
||||
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))
|
||||
|
||||
|
||||
|
||||
SectionCard(title = "Подробнее о вашем опыте работы:") {
|
||||
formState.value.workExperience.forEachIndexed { index, workExp ->
|
||||
Text(
|
||||
text = "№${index + 1}:",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp
|
||||
WorkExperienceForm(
|
||||
index = index,
|
||||
workExp = workExp,
|
||||
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))
|
||||
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()) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Пока ничего нет",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
EmptyStateText()
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
shape = Shapes.smallRoundedBox,
|
||||
AddItemButton(
|
||||
text = "Добавить",
|
||||
onClick = viewModel::addNewExperience,
|
||||
colors = ButtonColors(
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary,
|
||||
disabledContainerColor = colorScheme.onSecondary,
|
||||
disabledContentColor = colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
Card {
|
||||
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))
|
||||
|
||||
SectionCard(title = "Ваше образование:") {
|
||||
formState.value.education.forEachIndexed { index, education ->
|
||||
Text(
|
||||
text = "№${index + 1}:",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp
|
||||
EducationForm(
|
||||
index = index,
|
||||
education = education,
|
||||
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))
|
||||
|
||||
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()) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Пока ничего нет",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
EmptyStateText()
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
shape = Shapes.smallRoundedBox,
|
||||
AddItemButton(
|
||||
text = "Добавить",
|
||||
onClick = viewModel::addNewEducation,
|
||||
colors = ButtonColors(
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary,
|
||||
disabledContainerColor = colorScheme.onSecondary,
|
||||
disabledContentColor = colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
Card {
|
||||
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))
|
||||
|
||||
SectionCard(title = "Интересные проекты:") {
|
||||
formState.value.projects.forEachIndexed { index, project ->
|
||||
Text(
|
||||
text = "№${index + 1}:",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp
|
||||
ProjectForm(
|
||||
index = index,
|
||||
project = project,
|
||||
errors = formState.value.errors,
|
||||
onNameChange = { viewModel.changeProjectName(index, it) },
|
||||
onDescriptionChange = { viewModel.changeProjectDescription(index, it) },
|
||||
onRemove = { viewModel.removeProject(index) }
|
||||
)
|
||||
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()) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Пока ничего нет",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
EmptyStateText()
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
shape = Shapes.smallRoundedBox,
|
||||
AddItemButton(
|
||||
text = "Добавить",
|
||||
onClick = viewModel::addNewProject,
|
||||
colors = ButtonColors(
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary,
|
||||
disabledContainerColor = colorScheme.onSecondary,
|
||||
disabledContentColor = colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
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.launch
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
import kotlin.collections.minus
|
||||
|
||||
data class ResumeFormState(
|
||||
val about: String = "",
|
||||
val position: String = "",
|
||||
val experience: UIExperienceCount? = null,
|
||||
val experience: ExperienceType? = null,
|
||||
val keySkills: Set<String> = emptySet(),
|
||||
val city: String = "",
|
||||
val workExperience: List<WorkExperience> = emptyList(),
|
||||
val education: List<UIEducation> = emptyList(),
|
||||
val education: List<Education> = emptyList(),
|
||||
val projects: List<Project> = emptyList(),
|
||||
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
|
||||
class CreateResumeViewModel(
|
||||
private val suggestSkillsUseCase: SuggestSkillsUseCase,
|
||||
@@ -136,15 +74,9 @@ class CreateResumeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val experienceOptions = listOf(
|
||||
UIExperienceCount.NoExperience,
|
||||
UIExperienceCount.LessThan1,
|
||||
UIExperienceCount.Between1And3,
|
||||
UIExperienceCount.Between3And6,
|
||||
UIExperienceCount.MoreThan6
|
||||
)
|
||||
val experienceOptions = ExperienceType.entries.toList()
|
||||
|
||||
fun onExperienceSelect(value: UIExperienceCount) {
|
||||
fun onExperienceSelect(value: ExperienceType) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
experience = value,
|
||||
@@ -249,21 +181,14 @@ class CreateResumeViewModel(
|
||||
}
|
||||
|
||||
// Education
|
||||
val educationGradeOptions = listOf(
|
||||
UIEducationGrade.BasicGeneralEducation,
|
||||
UIEducationGrade.SecondaryGeneralEducation,
|
||||
UIEducationGrade.SecondaryProfessionalEducation,
|
||||
UIEducationGrade.Bachelor,
|
||||
UIEducationGrade.Specialist,
|
||||
UIEducationGrade.Master
|
||||
)
|
||||
val educationGradeOptions = EducationGrades.entries.toList()
|
||||
|
||||
fun addNewEducation() {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education + UIEducation(
|
||||
education = it.education + Education(
|
||||
place = "",
|
||||
grade = UIEducationGrade.Specialist,
|
||||
grade = EducationGrades.Specialist,
|
||||
specialization = "",
|
||||
description = ""
|
||||
)
|
||||
@@ -295,7 +220,7 @@ class CreateResumeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun changeEducationGrade(index: Int, value: UIEducationGrade) {
|
||||
fun changeEducationGrade(index: Int, value: EducationGrades) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education.mapIndexed { ind, education ->
|
||||
@@ -375,7 +300,7 @@ class CreateResumeViewModel(
|
||||
val validation = validateDataUseCase.validateResume(
|
||||
about = _formStateFillResume.value.about,
|
||||
position = _formStateFillResume.value.position,
|
||||
experience = _formStateFillResume.value.experience?.mapToDomain(),
|
||||
experience = _formStateFillResume.value.experience,
|
||||
keySkills = _formStateFillResume.value.keySkills.toList(),
|
||||
city = _formStateFillResume.value.city,
|
||||
workExperience = _formStateFillResume.value.workExperience,
|
||||
@@ -396,17 +321,10 @@ class CreateResumeViewModel(
|
||||
position = position,
|
||||
about = about,
|
||||
skills = keySkills.toList(),
|
||||
experienceType = experience!!.mapToDomain(),
|
||||
experienceType = experience!!,
|
||||
city = city,
|
||||
experience = workExperience,
|
||||
education = education.map {
|
||||
Education(
|
||||
place = it.place,
|
||||
grade = it.grade.mapToDomain(),
|
||||
specialization = it.specialization,
|
||||
description = it.description
|
||||
)
|
||||
},
|
||||
education = education,
|
||||
projects = projects
|
||||
)
|
||||
}
|
||||
|
||||
+350
-1
@@ -1,14 +1,58 @@
|
||||
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.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 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.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.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun ResumeDetailsScreen(
|
||||
fun ErrorCollectorScope.ResumeDetailsScreen(
|
||||
navBackStackEntry: NavBackStackEntry,
|
||||
viewModel: ResumeDetailsViewModel = koinViewModel {
|
||||
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 мес."
|
||||
}
|
||||
}
|
||||
+13
@@ -1,7 +1,10 @@
|
||||
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.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
import org.koin.core.annotation.Provided
|
||||
|
||||
@@ -10,4 +13,14 @@ class ResumeDetailsViewModel(
|
||||
@Provided resumeId: String,
|
||||
private val getResumeInfoUseCase: GetResumeInfoUseCase
|
||||
) : 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