From 0e0b007fc3d7e0030fff776ff868f261592904ce Mon Sep 17 00:00:00 2001 From: MaximOksiuta <63787095+MaximOksiuta@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:26:03 +0300 Subject: [PATCH] fix fix and fix --- .../data/data_providers/api/ApiKtorClient.kt | 25 +- .../moscow2025/data/dto/ResumeDtos.kt | 70 ++- .../repImplementations/AuthRepositoryImpl.kt | 2 + .../ResumeRepositoryImpl.kt | 3 +- .../moscow2025/domain/models/ResumeModel.kt | 6 +- .../usecase/resumes/GetResumeInfoUseCase.kt | 12 + .../usecase/resumes/LoadResumeListUseCase.kt | 58 +- .../components/standart/TTTextField.kt | 9 + .../dataModels/UIResumeBaseInfo.kt | 6 +- .../presentation/navigation/TTasksNavHost.kt | 2 +- .../createResume/CreateResumeScreen.kt | 521 ++++++++++-------- .../createResume/CreateResumeViewModel.kt | 31 +- .../presentation/screens/main/MainScreen.kt | 4 +- .../resumeDetails/ResumeDetailsScreen.kt | 2 - .../resumeDetails/ResumeDetailsViewModel.kt | 4 +- 15 files changed, 465 insertions(+), 290 deletions(-) create mode 100644 app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/GetResumeInfoUseCase.kt diff --git a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt index 3071978..b92afd5 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/data_providers/api/ApiKtorClient.kt @@ -1,5 +1,6 @@ package com.prodhack.moscow2025.data.data_providers.api +import android.util.Log import com.prodhack.moscow2025.common.Constants import com.prodhack.moscow2025.data.data_providers.localInfo.AuthorizationDataStore import io.ktor.client.HttpClient @@ -48,19 +49,21 @@ class ApiKtorClient(authorizationDataStore: AuthorizationDataStore) { } install(Auth) { bearer { - sendWithoutRequest { request -> - val segments = request.url.pathSegments - - val endpointsWithoutAuth = listOf( - "sign_in", - "sign_up" - ) - - endpointsWithoutAuth.any { segments.contains(it) }.not() - } +// sendWithoutRequest { request -> +// val segments = request.url.pathSegments +// +// val endpointsWithoutAuth = listOf( +// "sign_in", +// "sign_up" +// ) +// +// endpointsWithoutAuth.any { segments.contains(it) }.not() +// } loadTokens { return@loadTokens authorizationDataStore.token.first() - .toBearerTokens() + .toBearerTokens().also { + Log.d("csmlc", it.accessToken) + } } refreshTokens { CoroutineScope(Dispatchers.IO).launch { diff --git a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt index 2739f8a..87c1a72 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/dto/ResumeDtos.kt @@ -6,6 +6,7 @@ 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.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeModel import com.prodhack.moscow2025.domain.models.WorkExperience import kotlinx.serialization.SerialName @@ -55,11 +56,12 @@ data class ResumeDTO( @SerialName("key_skills") val keySkills: List, val position: String, + @SerialName("location") val city: String, - val experience: List, - val education: List, - val project: List, - val prediction: PredictionDTO + val experience: List = emptyList(), + val education: List = emptyList(), + val project: List = emptyList(), + val prediction: PredictionDTO? = null ) { fun mapToDomain(): ResumeModel = ResumeModel( id = id, @@ -67,11 +69,13 @@ data class ResumeDTO( skills = keySkills, position = position, experienceType = experienceType.mapToDomain(), - prediction = Pair( - prediction.fromSalary.toIntOrNull(), - prediction.toSalary.toIntOrNull() - ), - recommendedSkills = prediction.recommendedSkills, + prediction = prediction?.let { + Pair( + it.fromSalary.toIntOrNull(), + it.toSalary.toIntOrNull() + ) + }, + recommendedSkills = prediction?.recommendedSkills, city = city, experience = experience.map { it.mapToDomain() }, education = education.map { it.mapToDomain() }, @@ -83,9 +87,9 @@ data class ResumeDTO( aboutMe = aboutMe, keySkills = keySkills.joinToString("|"), position = position, - fromSalary = prediction.fromSalary.toIntOrNull(), - toSalary = prediction.toSalary.toIntOrNull(), - recommendedSkills = prediction.recommendedSkills.joinToString("|"), + fromSalary = prediction?.fromSalary?.toIntOrNull(), + toSalary = prediction?.toSalary?.toIntOrNull(), + recommendedSkills = prediction?.recommendedSkills?.joinToString("|") ?: "", experienceType = experienceType.mapToDomain().name, city = city, 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 data class EducationDTO( 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 enum class EducationGradesDTO { @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 data class ProjectDTO( val name: String, @@ -172,6 +201,11 @@ data class ProjectDTO( ) } +fun Project.mapToData(): ProjectDTO = ProjectDTO( + name = name, + description = description +) + @Serializable data class ResumeCreateDTO( @SerialName("experience_type") @@ -181,12 +215,24 @@ data class ResumeCreateDTO( @SerialName("key_skills") val keySkills: List, val position: String, + @SerialName("location") val city: String, val experience: List, val education: List, val project: List, ) +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 data class PredictionDTO( @SerialName("from_salary") diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt index f3404a1..cfe078e 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/AuthRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.prodhack.moscow2025.data.repImplementations +import android.util.Log import com.prodhack.moscow2025.data.base.BaseRepository import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient 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.contentType import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import org.koin.core.annotation.Single diff --git a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt index 52bc2c6..b539422 100644 --- a/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt +++ b/app/src/main/java/com/prodhack/moscow2025/data/repImplementations/ResumeRepositoryImpl.kt @@ -8,6 +8,7 @@ import com.prodhack.moscow2025.data.dto.ResumeCreateDTO import com.prodhack.moscow2025.data.dto.ResumeIdDTO import com.prodhack.moscow2025.data.dto.ResumeListDTO 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.models.ResumeCreationModel import com.prodhack.moscow2025.domain.models.ResumeModel @@ -64,7 +65,7 @@ class ResumeRepositoryImpl( url("/resume") } - setBody(ResumeCreateDTO) + setBody(resumeForm.mapToData()) contentType(ContentType.Application.Json) }.map { it.resumeId } } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt index a0df88d..1126015 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/models/ResumeModel.kt @@ -10,15 +10,15 @@ data class ResumeModel( val experience: List, val education: List, val projects: List, - val prediction: Pair, - val recommendedSkills: List + val prediction: Pair?, + val recommendedSkills: List? ) data class ResumeCreationModel( val position: String, val about: String, val skills: List, - val city: String?, + val city: String, val experienceType: ExperienceType, val experience: List, val education: List, diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/GetResumeInfoUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/GetResumeInfoUseCase.kt new file mode 100644 index 0000000..f772a4f --- /dev/null +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/GetResumeInfoUseCase.kt @@ -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> { + TODO() + } +} diff --git a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt index 4f145ec..837c3b1 100644 --- a/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt +++ b/app/src/main/java/com/prodhack/moscow2025/domain/usecase/resumes/LoadResumeListUseCase.kt @@ -10,35 +10,35 @@ import org.koin.core.annotation.Single @Single class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) { -// operator fun invoke(): RemotePagingWrapper = resumeRepository.loadResumeList() + operator fun invoke(): RemotePagingWrapper = resumeRepository.loadResumeList() // Mocked data - operator fun invoke(): RemotePagingWrapper = flow { - emit( - PagingData.from( - listOf( - ResumeModel( - id = "iajxioasdkmcaolsd,c", - position = "Android разработчик", - about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " + - "И нет это я не про себя, это просто какие-то данные," + - " чтобы проверить, что это чудовище работает", - skills = listOf( - "Android SDK", - "Kotlin", - "Room", - "Ktor" - ), - experienceType = ExperienceType.Between3And6, - city = "Moscow", - experience = listOf(), - education = listOf(), - projects = listOf(), - prediction = Pair(200000, 230000), - recommendedSkills = listOf("KMP") - ) - ) - ) - ) - } +// operator fun invoke(): RemotePagingWrapper = flow { +// emit( +// PagingData.from( +// listOf( +// ResumeModel( +// id = "iajxioasdkmcaolsd,c", +// position = "Android разработчик", +// about = "Ну оооочень крутой андроид разраб, с огромным количеством опыта. " + +// "И нет это я не про себя, это просто какие-то данные," + +// " чтобы проверить, что это чудовище работает", +// skills = listOf( +// "Android SDK", +// "Kotlin", +// "Room", +// "Ktor" +// ), +// experienceType = ExperienceType.Between3And6, +// city = "Moscow", +// experience = listOf(), +// education = listOf(), +// projects = listOf(), +// prediction = Pair(200000, 230000), +// recommendedSkills = listOf("KMP") +// ) +// ) +// ) +// ) +// } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt index 07dc14c..c1cd91c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/components/standart/TTTextField.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape 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.sp import com.prodhack.moscow2025.R +import com.prodhack.moscow2025.presentation.theme.Paddings import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) @@ -254,6 +256,10 @@ fun TTTextFieldWithDropdown( } ) } + + if (dropdownItems.isEmpty()){ + Text(modifier = Modifier.padding(Paddings.small), text = "Ничего нет", style = typography.labelLarge, fontSize = 16.sp) + } } } } @@ -362,6 +368,9 @@ fun TTTextFieldWithSearch( } ) } + if (dropdownItems.isEmpty()){ + Text(modifier = Modifier.padding(Paddings.small), text = "Ничего не нашлось", style = typography.labelLarge, fontSize = 16.sp) + } } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt index c7321f1..d74f0f7 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/dataModels/UIResumeBaseInfo.kt @@ -11,7 +11,7 @@ data class UIResumeBaseInfo( fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo( id = id, positionName = position, - salary = prediction.first?.let { from -> - prediction.second?.let { to -> "$from-$to" } ?: from.toString() - } ?: prediction.second?.toString() ?: "Ошибка" + salary = prediction?.first?.let { from -> + prediction.second?.let { to -> "$from-$to₽" } ?: "$from₽" + } ?: prediction?.second?.let { "$it₽" } ?: "Загрузка..." ) \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt index effffa0..6211ce4 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/navigation/TTasksNavHost.kt @@ -118,7 +118,7 @@ fun TTasksNavHost( } composable(AppDestination.ResumeCreation.route) { - CreateResumeScreen() + CreateResumeScreen({ navController.popBackStack() }) } } } diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt index f9284b8..cc699b5 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel @Composable fun CreateResumeScreen( + goBack: () -> Unit, viewModel: CreateResumeViewModel = koinViewModel() ) { val colorScheme = MaterialTheme.colorScheme @@ -64,7 +65,8 @@ fun CreateResumeScreen( Icon( modifier = Modifier .rotate(180f) - .size(24.dp), + .size(24.dp) + .noRippleClickable(goBack), painter = painterResource(R.drawable.ic_arr_details), tint = colorScheme.onBackground, contentDescription = "go back" @@ -114,7 +116,7 @@ fun CreateResumeScreen( error = formState.value.errors[ResumeField.Experience], dropdownItems = viewModel.experienceOptions, dropDownItem = { - Text(text = it.friendlyName, style = typography.titleMedium, fontSize = 16.sp) + Text(text = it.friendlyName, style = typography.labelLarge, fontSize = 16.sp) }, onDropdownItemSelected = viewModel::onExperienceSelect ) @@ -179,243 +181,316 @@ fun CreateResumeScreen( 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)) + 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.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()) { - 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 = { + formState.value.workExperience.forEachIndexed { index, workExp -> Text( - text = it.friendlyName, + text = "№${index + 1}:", style = typography.labelLarge, - fontSize = 16.sp + fontSize = 18.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)) - } + 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.education.isEmpty()) { - Text( - modifier = Modifier.fillMaxWidth(), - text = "Пока ничего нет", - style = typography.labelLarge, - fontSize = 18.sp, - textAlign = TextAlign.Center - ) - 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 + ) + 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, - ) + 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)) + + 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)) - Text( - modifier = Modifier.fillMaxWidth(), - text = "Интересные проекты:", - style = typography.titleMedium, - fontSize = 20.sp, - textAlign = TextAlign.Center - ) + 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)) + Spacer(modifier = Modifier.height(Paddings.large)) - formState.value.projects.forEachIndexed { index, project -> - Text( - text = "№${index + 1}:", - style = typography.labelLarge, - fontSize = 18.sp - ) - Spacer(modifier = Modifier.height(Paddings.medium)) + formState.value.projects.forEachIndexed { index, project -> + Text( + text = "№${index + 1}:", + style = typography.labelLarge, + fontSize = 18.sp + ) + 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)) - } + 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 - ) - Spacer(modifier = Modifier.height(Paddings.medium)) - } + if (formState.value.projects.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::addNewProject, - colors = ButtonColors( - containerColor = colorScheme.onSecondary, - contentColor = colorScheme.secondary, - disabledContainerColor = colorScheme.onSecondary, - disabledContentColor = colorScheme.secondary - ) - ) { - Text( - text = "Добавить", - style = typography.labelLarge, - fontSize = 18.sp, - ) + Button( + modifier = Modifier + .fillMaxWidth(), + shape = Shapes.smallRoundedBox, + 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, + ) + } + } } Spacer(modifier = Modifier.height(Paddings.large)) diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt index d8c0848..10ec421 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/createResume/CreateResumeViewModel.kt @@ -80,9 +80,9 @@ sealed class UIEducationGrade(val friendlyName: String) { data object Specialist : 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) { BasicGeneralEducation -> EducationGrades.BasicGeneralEducation @@ -161,6 +161,7 @@ class CreateResumeViewModel( errors = it.errors - ResumeField.KeySkills ) } + skillSearchQuery.value = "" } 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) { _formStateFillResume.update { 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) { _formStateFillResume.update { it.copy( @@ -372,7 +397,7 @@ class CreateResumeViewModel( about = about, skills = keySkills.toList(), experienceType = experience!!.mapToDomain(), - city = city.ifBlank { null }, + city = city, experience = workExperience, education = education.map { Education( diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt index 9150254..9ef0a61 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/main/MainScreen.kt @@ -79,6 +79,8 @@ fun ErrorCollectorScope.MainScreen( color = colorScheme.onBackground ) + Spacer(modifier = Modifier.height(Paddings.large)) + BigButton( onClick = { TODO() @@ -165,7 +167,7 @@ fun ResumeShortInfoCard( fontSize = 18.sp ) Text( - "${info.salary}₽", + info.salary, style = typography.titleMedium, color = MaterialTheme.colorScheme.primary, fontSize = 18.sp diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt index d2be0de..2d7d65c 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsScreen.kt @@ -17,6 +17,4 @@ fun ResumeDetailsScreen( } ) { - Text("Opened resume details for id ${navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""}") - } \ No newline at end of file diff --git a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt index 69e1744..ce29c78 100644 --- a/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt +++ b/app/src/main/java/com/prodhack/moscow2025/presentation/screens/resumeDetails/ResumeDetailsViewModel.kt @@ -1,11 +1,13 @@ package com.prodhack.moscow2025.presentation.screens.resumeDetails +import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel import org.koin.android.annotation.KoinViewModel import org.koin.core.annotation.Provided @KoinViewModel class ResumeDetailsViewModel( - @Provided resumeId: String + @Provided resumeId: String, + private val getResumeInfoUseCase: GetResumeInfoUseCase ) : BaseViewModel() { } \ No newline at end of file