fix fix and fix

This commit is contained in:
MaximOksiuta
2025-11-22 19:26:03 +03:00
parent 584338a1de
commit 0e0b007fc3
15 changed files with 465 additions and 290 deletions
@@ -1,5 +1,6 @@
package com.prodhack.moscow2025.data.data_providers.api package com.prodhack.moscow2025.data.data_providers.api
import android.util.Log
import com.prodhack.moscow2025.common.Constants import com.prodhack.moscow2025.common.Constants
import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
@@ -48,19 +49,21 @@ class ApiKtorClient(authorizationDataStore: AuthorizationDataStore) {
} }
install(Auth) { install(Auth) {
bearer { bearer {
sendWithoutRequest { request -> // sendWithoutRequest { request ->
val segments = request.url.pathSegments // val segments = request.url.pathSegments
//
val endpointsWithoutAuth = listOf( // val endpointsWithoutAuth = listOf(
"sign_in", // "sign_in",
"sign_up" // "sign_up"
) // )
//
endpointsWithoutAuth.any { segments.contains(it) }.not() // endpointsWithoutAuth.any { segments.contains(it) }.not()
} // }
loadTokens { loadTokens {
return@loadTokens authorizationDataStore.token.first() return@loadTokens authorizationDataStore.token.first()
.toBearerTokens() .toBearerTokens().also {
Log.d("csmlc", it.accessToken)
}
} }
refreshTokens { refreshTokens {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -6,6 +6,7 @@ import com.prodhack.moscow2025.domain.models.Education
import com.prodhack.moscow2025.domain.models.EducationGrades import com.prodhack.moscow2025.domain.models.EducationGrades
import com.prodhack.moscow2025.domain.models.ExperienceType import com.prodhack.moscow2025.domain.models.ExperienceType
import com.prodhack.moscow2025.domain.models.Project import com.prodhack.moscow2025.domain.models.Project
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.models.WorkExperience import com.prodhack.moscow2025.domain.models.WorkExperience
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -55,11 +56,12 @@ data class ResumeDTO(
@SerialName("key_skills") @SerialName("key_skills")
val keySkills: List<String>, val keySkills: List<String>,
val position: String, val position: String,
@SerialName("location")
val city: String, val city: String,
val experience: List<ExperienceDTO>, val experience: List<ExperienceDTO> = emptyList(),
val education: List<EducationDTO>, val education: List<EducationDTO> = emptyList(),
val project: List<ProjectDTO>, val project: List<ProjectDTO> = emptyList(),
val prediction: PredictionDTO val prediction: PredictionDTO? = null
) { ) {
fun mapToDomain(): ResumeModel = ResumeModel( fun mapToDomain(): ResumeModel = ResumeModel(
id = id, id = id,
@@ -67,11 +69,13 @@ data class ResumeDTO(
skills = keySkills, skills = keySkills,
position = position, position = position,
experienceType = experienceType.mapToDomain(), experienceType = experienceType.mapToDomain(),
prediction = Pair( prediction = prediction?.let {
prediction.fromSalary.toIntOrNull(), Pair(
prediction.toSalary.toIntOrNull() it.fromSalary.toIntOrNull(),
), it.toSalary.toIntOrNull()
recommendedSkills = prediction.recommendedSkills, )
},
recommendedSkills = prediction?.recommendedSkills,
city = city, city = city,
experience = experience.map { it.mapToDomain() }, experience = experience.map { it.mapToDomain() },
education = education.map { it.mapToDomain() }, education = education.map { it.mapToDomain() },
@@ -83,9 +87,9 @@ data class ResumeDTO(
aboutMe = aboutMe, aboutMe = aboutMe,
keySkills = keySkills.joinToString("|"), keySkills = keySkills.joinToString("|"),
position = position, position = position,
fromSalary = prediction.fromSalary.toIntOrNull(), fromSalary = prediction?.fromSalary?.toIntOrNull(),
toSalary = prediction.toSalary.toIntOrNull(), toSalary = prediction?.toSalary?.toIntOrNull(),
recommendedSkills = prediction.recommendedSkills.joinToString("|"), recommendedSkills = prediction?.recommendedSkills?.joinToString("|") ?: "",
experienceType = experienceType.mapToDomain().name, experienceType = experienceType.mapToDomain().name,
city = city, city = city,
experience = JsonTypeConverters.fromWorkExperienceList(experience.map { it.mapToDomain() }), experience = JsonTypeConverters.fromWorkExperienceList(experience.map { it.mapToDomain() }),
@@ -108,6 +112,12 @@ data class ExperienceDTO(
) )
} }
fun WorkExperience.mapToData(): ExperienceDTO = ExperienceDTO(
place = place,
description = description,
monthDuration = monthDuration ?: 0
)
@Serializable @Serializable
data class EducationDTO( data class EducationDTO(
val place: String, val place: String,
@@ -123,6 +133,13 @@ data class EducationDTO(
) )
} }
fun Education.mapToData(): EducationDTO = EducationDTO(
place = place,
grade = grade.mapToData(),
specialization = specialization,
description = description
)
@Serializable @Serializable
enum class EducationGradesDTO { enum class EducationGradesDTO {
@SerialName("basic_general_education") @SerialName("basic_general_education")
@@ -161,6 +178,18 @@ enum class EducationGradesDTO {
} }
} }
fun EducationGrades.mapToData(): EducationGradesDTO =
when (this) {
EducationGrades.BasicGeneralEducation -> EducationGradesDTO.BasicGeneralEducation
EducationGrades.SecondaryGeneralEducation -> EducationGradesDTO.SecondaryGeneralEducation
EducationGrades.SecondaryProfessionalEducation -> EducationGradesDTO.SecondaryProfessionalEducation
EducationGrades.Bachelor -> EducationGradesDTO.Bachelor
EducationGrades.Specialist -> EducationGradesDTO.Specialist
EducationGrades.Master -> EducationGradesDTO.Master
EducationGrades.PostgraduateStudies -> EducationGradesDTO.PostgraduateStudies
EducationGrades.Other -> EducationGradesDTO.Other
}
@Serializable @Serializable
data class ProjectDTO( data class ProjectDTO(
val name: String, val name: String,
@@ -172,6 +201,11 @@ data class ProjectDTO(
) )
} }
fun Project.mapToData(): ProjectDTO = ProjectDTO(
name = name,
description = description
)
@Serializable @Serializable
data class ResumeCreateDTO( data class ResumeCreateDTO(
@SerialName("experience_type") @SerialName("experience_type")
@@ -181,12 +215,24 @@ data class ResumeCreateDTO(
@SerialName("key_skills") @SerialName("key_skills")
val keySkills: List<String>, val keySkills: List<String>,
val position: String, val position: String,
@SerialName("location")
val city: String, val city: String,
val experience: List<ExperienceDTO>, val experience: List<ExperienceDTO>,
val education: List<EducationDTO>, val education: List<EducationDTO>,
val project: List<ProjectDTO>, val project: List<ProjectDTO>,
) )
fun ResumeCreationModel.mapToData(): ResumeCreateDTO = ResumeCreateDTO(
experienceType = experienceType.mapToData(),
aboutMe = about,
keySkills = skills,
position = position,
city = city,
experience = experience.map { it.mapToData() },
education = education.map { it.mapToData() },
project = projects.map { it.mapToData() }
)
@Serializable @Serializable
data class PredictionDTO( data class PredictionDTO(
@SerialName("from_salary") @SerialName("from_salary")
@@ -1,5 +1,6 @@
package com.prodhack.moscow2025.data.repImplementations package com.prodhack.moscow2025.data.repImplementations
import android.util.Log
import com.prodhack.moscow2025.data.base.BaseRepository 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.localInfo.AuthorizationDataStore import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore
@@ -14,6 +15,7 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.contentType import io.ktor.http.contentType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
@@ -8,6 +8,7 @@ import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
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
import com.prodhack.moscow2025.data.dto.mapToData
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository 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
@@ -64,7 +65,7 @@ class ResumeRepositoryImpl(
url("/resume") url("/resume")
} }
setBody(ResumeCreateDTO) setBody(resumeForm.mapToData())
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
}.map { it.resumeId } }.map { it.resumeId }
} }
@@ -10,15 +10,15 @@ data class ResumeModel(
val experience: List<WorkExperience>, val experience: List<WorkExperience>,
val education: List<Education>, val education: List<Education>,
val projects: List<Project>, val projects: List<Project>,
val prediction: Pair<Int?, Int?>, val prediction: Pair<Int?, Int?>?,
val recommendedSkills: List<String> val recommendedSkills: List<String>?
) )
data class ResumeCreationModel( data class ResumeCreationModel(
val position: String, val position: String,
val about: String, val about: String,
val skills: List<String>, val skills: List<String>,
val city: String?, val city: String,
val experienceType: ExperienceType, val experienceType: ExperienceType,
val experience: List<WorkExperience>, val experience: List<WorkExperience>,
val education: List<Education>, val education: List<Education>,
@@ -0,0 +1,12 @@
package com.prodhack.moscow2025.domain.usecase.resumes
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()
}
}
@@ -10,35 +10,35 @@ import org.koin.core.annotation.Single
@Single @Single
class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) { class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) {
// operator fun invoke(): RemotePagingWrapper<ResumeModel> = resumeRepository.loadResumeList() operator fun invoke(): RemotePagingWrapper<ResumeModel> = resumeRepository.loadResumeList()
// Mocked data // Mocked data
operator fun invoke(): RemotePagingWrapper<ResumeModel> = flow { // operator fun invoke(): RemotePagingWrapper<ResumeModel> = flow {
emit( // emit(
PagingData.from( // PagingData.from(
listOf( // listOf(
ResumeModel( // ResumeModel(
id = "iajxioasdkmcaolsd,c", // id = "iajxioasdkmcaolsd,c",
position = "Android разработчик", // position = "Android разработчик",
about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " + // about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " +
"И нет это я не про себя, это просто какие-то данные," + // "И нет это я не про себя, это просто какие-то данные," +
" чтобы проверить, что это чудовище работает", // " чтобы проверить, что это чудовище работает",
skills = listOf( // skills = listOf(
"Android SDK", // "Android SDK",
"Kotlin", // "Kotlin",
"Room", // "Room",
"Ktor" // "Ktor"
), // ),
experienceType = ExperienceType.Between3And6, // experienceType = ExperienceType.Between3And6,
city = "Moscow", // city = "Moscow",
experience = listOf(), // experience = listOf(),
education = listOf(), // education = listOf(),
projects = listOf(), // projects = listOf(),
prediction = Pair(200000, 230000), // prediction = Pair(200000, 230000),
recommendedSkills = listOf("KMP") // recommendedSkills = listOf("KMP")
) // )
) // )
) // )
) // )
} // }
} }
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -36,6 +37,7 @@ import androidx.compose.ui.text.input.VisualTransformation
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.presentation.theme.Paddings
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class)
@@ -254,6 +256,10 @@ fun <T> TTTextFieldWithDropdown(
} }
) )
} }
if (dropdownItems.isEmpty()){
Text(modifier = Modifier.padding(Paddings.small), text = "Ничего нет", style = typography.labelLarge, fontSize = 16.sp)
}
} }
} }
} }
@@ -362,6 +368,9 @@ fun <T> TTTextFieldWithSearch(
} }
) )
} }
if (dropdownItems.isEmpty()){
Text(modifier = Modifier.padding(Paddings.small), text = "Ничего не нашлось", style = typography.labelLarge, fontSize = 16.sp)
}
} }
} }
} }
@@ -11,7 +11,7 @@ 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?.first?.let { from ->
prediction.second?.let { to -> "$from-$to" } ?: from.toString() prediction.second?.let { to -> "$from-$to" } ?: "$from"
} ?: prediction.second?.toString() ?: "Ошибка" } ?: prediction?.second?.let { "$it" } ?: "Загрузка..."
) )
@@ -118,7 +118,7 @@ fun TTasksNavHost(
} }
composable(AppDestination.ResumeCreation.route) { composable(AppDestination.ResumeCreation.route) {
CreateResumeScreen() CreateResumeScreen({ navController.popBackStack() })
} }
} }
} }
@@ -15,7 +15,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonColors
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Card
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable @Composable
fun CreateResumeScreen( fun CreateResumeScreen(
goBack: () -> Unit,
viewModel: CreateResumeViewModel = koinViewModel() viewModel: CreateResumeViewModel = koinViewModel()
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
@@ -64,7 +65,8 @@ fun CreateResumeScreen(
Icon( Icon(
modifier = Modifier modifier = Modifier
.rotate(180f) .rotate(180f)
.size(24.dp), .size(24.dp)
.noRippleClickable(goBack),
painter = painterResource(R.drawable.ic_arr_details), painter = painterResource(R.drawable.ic_arr_details),
tint = colorScheme.onBackground, tint = colorScheme.onBackground,
contentDescription = "go back" contentDescription = "go back"
@@ -114,7 +116,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.titleMedium, fontSize = 16.sp) Text(text = it.friendlyName, style = typography.labelLarge, fontSize = 16.sp)
}, },
onDropdownItemSelected = viewModel::onExperienceSelect onDropdownItemSelected = viewModel::onExperienceSelect
) )
@@ -179,243 +181,316 @@ fun CreateResumeScreen(
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
Text( Card {
modifier = Modifier.fillMaxWidth(), Column(modifier = Modifier.padding(Paddings.medium)) {
text = "Подробнее о вашем опыте работы:", Text(
style = typography.titleMedium, modifier = Modifier.fillMaxWidth(),
fontSize = 20.sp, text = "Подробнее о вашем опыте работы:",
textAlign = TextAlign.Center style = typography.titleMedium,
) fontSize = 20.sp,
textAlign = TextAlign.Center
Spacer(modifier = Modifier.height(Paddings.large)) )
Spacer(modifier = Modifier.height(Paddings.large))
formState.value.workExperience.forEachIndexed { index, workExp ->
Text(
text = "${index + 1}:",
style = typography.labelLarge,
fontSize = 18.sp
)
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))
}
if (formState.value.workExperience.isEmpty()) { formState.value.workExperience.forEachIndexed { index, workExp ->
Text(
modifier = Modifier.fillMaxWidth(),
text = "Пока ничего нет",
style = typography.labelLarge,
fontSize = 18.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
Button(
modifier = Modifier
.fillMaxWidth(),
shape = Shapes.smallRoundedBox,
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,
)
}
Spacer(modifier = Modifier.height(Paddings.large))
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 ->
Text(
text = "${index + 1}:",
style = typography.labelLarge,
fontSize = 18.sp
)
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(
text = it.friendlyName, text = "${index + 1}:",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 16.sp fontSize = 18.sp
) )
}, Spacer(modifier = Modifier.height(Paddings.medium))
onDropdownItemSelected = { viewModel.changeEducationGrade(index, it) } TTTextField(
) value = workExp.place,
Spacer(modifier = Modifier.height(Paddings.medium)) onValueChange = {
TTTextField( viewModel.changeWorkExperiencePlace(index, it)
value = education.specialization, },
onValueChange = { viewModel.changeEducationSpecialization(index, it) }, label = "Место работы",
label = "Специализация", error = formState.value.errors[ResumeField.WorkExperiencePlace(index)]
error = formState.value.errors[ResumeField.EducationSpecialization(index)] )
) Spacer(modifier = Modifier.height(Paddings.medium))
Spacer(modifier = Modifier.height(Paddings.medium)) TTTextField(
TTTextField( value = workExp.description,
value = education.description, onValueChange = {
onValueChange = { viewModel.changeEducationDescription(index, it) }, viewModel.changeWorkExperienceDescription(index, it)
singleLine = false, },
maxLines = 10, singleLine = false,
label = "Расскажите подробнее (опционально)", maxLines = 10,
error = formState.value.errors[ResumeField.EducationDescription(index)] label = "Расскажите подробнее",
) error = formState.value.errors[ResumeField.WorkExperienceDescription(
Spacer(modifier = Modifier.height(Paddings.medium)) 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.education.isEmpty()) { if (formState.value.workExperience.isEmpty()) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = "Пока ничего нет", text = "Пока ничего нет",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp, fontSize = 18.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(Paddings.medium)) Spacer(modifier = Modifier.height(Paddings.medium))
} }
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
shape = Shapes.smallRoundedBox, shape = Shapes.smallRoundedBox,
onClick = viewModel::addNewEducation, onClick = viewModel::addNewExperience,
colors = ButtonColors( colors = ButtonColors(
containerColor = colorScheme.onSecondary, containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary, contentColor = colorScheme.secondary,
disabledContainerColor = colorScheme.onSecondary, disabledContainerColor = colorScheme.onSecondary,
disabledContentColor = colorScheme.secondary disabledContentColor = colorScheme.secondary
) )
) { ) {
Text( Text(
text = "Добавить", text = "Добавить",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp, fontSize = 18.sp,
) )
}
}
}
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))
formState.value.education.forEachIndexed { index, education ->
Text(
text = "${index + 1}:",
style = typography.labelLarge,
fontSize = 18.sp
)
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
)
Spacer(modifier = Modifier.height(Paddings.medium))
}
Button(
modifier = Modifier
.fillMaxWidth(),
shape = Shapes.smallRoundedBox,
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,
)
}
}
} }
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
Text( Card {
modifier = Modifier.fillMaxWidth(), Column(modifier = Modifier.padding(Paddings.medium)) {
text = "Интересные проекты:", Text(
style = typography.titleMedium, modifier = Modifier.fillMaxWidth(),
fontSize = 20.sp, text = "Интересные проекты:",
textAlign = TextAlign.Center style = typography.titleMedium,
) fontSize = 20.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
formState.value.projects.forEachIndexed { index, project -> formState.value.projects.forEachIndexed { index, project ->
Text( Text(
text = "${index + 1}:", text = "${index + 1}:",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp fontSize = 18.sp
) )
Spacer(modifier = Modifier.height(Paddings.medium)) Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = project.name, value = project.name,
onValueChange = { viewModel.changeProjectName(index, it) }, onValueChange = { viewModel.changeProjectName(index, it) },
label = "Название проекта", label = "Название проекта",
error = formState.value.errors[ResumeField.ProjectName(index)] error = formState.value.errors[ResumeField.ProjectName(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium)) Spacer(modifier = Modifier.height(Paddings.medium))
TTTextField( TTTextField(
value = project.description, value = project.description,
onValueChange = { viewModel.changeProjectDescription(index, it) }, onValueChange = { viewModel.changeProjectDescription(index, it) },
singleLine = false, singleLine = false,
maxLines = 10, maxLines = 10,
label = "Расскажите подробнее", label = "Расскажите подробнее",
error = formState.value.errors[ResumeField.ProjectDescription(index)] error = formState.value.errors[ResumeField.ProjectDescription(index)]
) )
Spacer(modifier = Modifier.height(Paddings.medium)) 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( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = "Пока ничего нет", text = "Пока ничего нет",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp, fontSize = 18.sp,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(Paddings.medium)) Spacer(modifier = Modifier.height(Paddings.medium))
} }
Button( Button(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
shape = Shapes.smallRoundedBox, shape = Shapes.smallRoundedBox,
onClick = viewModel::addNewProject, onClick = viewModel::addNewProject,
colors = ButtonColors( colors = ButtonColors(
containerColor = colorScheme.onSecondary, containerColor = colorScheme.onSecondary,
contentColor = colorScheme.secondary, contentColor = colorScheme.secondary,
disabledContainerColor = colorScheme.onSecondary, disabledContainerColor = colorScheme.onSecondary,
disabledContentColor = colorScheme.secondary disabledContentColor = colorScheme.secondary
) )
) { ) {
Text( Text(
text = "Добавить", text = "Добавить",
style = typography.labelLarge, style = typography.labelLarge,
fontSize = 18.sp, fontSize = 18.sp,
) )
}
}
} }
Spacer(modifier = Modifier.height(Paddings.large)) Spacer(modifier = Modifier.height(Paddings.large))
@@ -80,9 +80,9 @@ sealed class UIEducationGrade(val friendlyName: String) {
data object Specialist : UIEducationGrade("Специалитет") data object Specialist : UIEducationGrade("Специалитет")
data object Master : UIEducationGrade("Магистратура") data object Master : UIEducationGrade("Магистратура")
data object PostgraduateStudies: UIEducationGrade("Аспирантура и выше") data object PostgraduateStudies : UIEducationGrade("Аспирантура и выше")
data object Other: UIEducationGrade("Другое") data object Other : UIEducationGrade("Другое")
fun mapToDomain(): EducationGrades = when (this) { fun mapToDomain(): EducationGrades = when (this) {
BasicGeneralEducation -> EducationGrades.BasicGeneralEducation BasicGeneralEducation -> EducationGrades.BasicGeneralEducation
@@ -161,6 +161,7 @@ class CreateResumeViewModel(
errors = it.errors - ResumeField.KeySkills errors = it.errors - ResumeField.KeySkills
) )
} }
skillSearchQuery.value = ""
} }
fun onRemoveSkill(value: String) { fun onRemoveSkill(value: String) {
@@ -270,6 +271,19 @@ class CreateResumeViewModel(
} }
} }
fun removeEducation(id: Int) {
_formStateFillResume.update {
it.copy(
education = it.education.filterIndexed { index, _ -> index != id },
errors = it.errors
- ResumeField.EducationSpecialization(id)
- ResumeField.EducationDescription(id)
- ResumeField.EducationPlace(id)
- ResumeField.EducationGrade(id)
)
}
}
fun changeEducationPlace(index: Int, value: String) { fun changeEducationPlace(index: Int, value: String) {
_formStateFillResume.update { _formStateFillResume.update {
it.copy( it.copy(
@@ -323,6 +337,17 @@ class CreateResumeViewModel(
} }
} }
fun removeProject(id: Int) {
_formStateFillResume.update {
it.copy(
projects = it.projects.filterIndexed { index, _ -> index != id },
errors = it.errors
- ResumeField.ProjectDescription(id)
- ResumeField.ProjectName(id)
)
}
}
fun changeProjectName(index: Int, value: String) { fun changeProjectName(index: Int, value: String) {
_formStateFillResume.update { _formStateFillResume.update {
it.copy( it.copy(
@@ -372,7 +397,7 @@ class CreateResumeViewModel(
about = about, about = about,
skills = keySkills.toList(), skills = keySkills.toList(),
experienceType = experience!!.mapToDomain(), experienceType = experience!!.mapToDomain(),
city = city.ifBlank { null }, city = city,
experience = workExperience, experience = workExperience,
education = education.map { education = education.map {
Education( Education(
@@ -79,6 +79,8 @@ fun ErrorCollectorScope.MainScreen(
color = colorScheme.onBackground color = colorScheme.onBackground
) )
Spacer(modifier = Modifier.height(Paddings.large))
BigButton( BigButton(
onClick = { onClick = {
TODO() TODO()
@@ -165,7 +167,7 @@ fun ResumeShortInfoCard(
fontSize = 18.sp fontSize = 18.sp
) )
Text( Text(
"${info.salary}", info.salary,
style = typography.titleMedium, style = typography.titleMedium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontSize = 18.sp fontSize = 18.sp
@@ -17,6 +17,4 @@ fun ResumeDetailsScreen(
} }
) { ) {
Text("Opened resume details for id ${navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""}")
} }
@@ -1,11 +1,13 @@
package com.prodhack.moscow2025.presentation.screens.resumeDetails package com.prodhack.moscow2025.presentation.screens.resumeDetails
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
import org.koin.android.annotation.KoinViewModel import org.koin.android.annotation.KoinViewModel
import org.koin.core.annotation.Provided import org.koin.core.annotation.Provided
@KoinViewModel @KoinViewModel
class ResumeDetailsViewModel( class ResumeDetailsViewModel(
@Provided resumeId: String @Provided resumeId: String,
private val getResumeInfoUseCase: GetResumeInfoUseCase
) : BaseViewModel() { ) : BaseViewModel() {
} }