You've already forked RekomenciMobile
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59e7d09693 | |||
| 291fc43470 | |||
| 0e0b007fc3 | |||
| 584338a1de | |||
|
a8f77e22b2
|
|||
|
ad6a442fba
|
|||
|
a2c0b47a3c
|
|||
|
cfb19a6c1e
|
|||
|
cddd44b197
|
+29
-29
@@ -1,46 +1,46 @@
|
|||||||
image: docker.io/eclipse-temurin:21
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
|
|
||||||
cache:
|
image: eclipse-temurin:21-jdk
|
||||||
key: ${CI_COMMIT_REF_SLUG}
|
|
||||||
paths:
|
variables:
|
||||||
- .gradle/
|
ANDROID_COMPILE_SDK: "36"
|
||||||
- $HOME/Android/
|
ANDROID_BUILD_TOOLS: "36.0.0"
|
||||||
|
ANDROID_SDK_TOOLS: "11076708"
|
||||||
|
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||||
|
ANDROID_SDK_ROOT: "$CI_PROJECT_DIR/android-home"
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- apt-get update -qq && apt-get install -y wget tar unzip lib32stdc++6 lib32z1
|
||||||
- apt-get install -y wget unzip git
|
- export ANDROID_SDK_ROOT="${PWD}/android-home"
|
||||||
- wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O cmdline-tools.zip
|
- mkdir -p $ANDROID_SDK_ROOT/cmdline-tools
|
||||||
- mkdir -p $HOME/Android/cmdline-tools
|
- wget -q -O $ANDROID_SDK_ROOT/cmdline-tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip"
|
||||||
- unzip cmdline-tools.zip -d $HOME/Android/cmdline-tools
|
- unzip -d $ANDROID_SDK_ROOT/cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools.zip
|
||||||
- yes | $HOME/Android/cmdline-tools/cmdline-tools/bin/sdkmanager --sdk_root=$HOME/Android "platform-tools" "platforms;android-33" "build-tools;33.0.2"
|
- mv $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/tools || true
|
||||||
- export ANDROID_HOME=$HOME/Android
|
- export PATH=$PATH:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin/
|
||||||
- export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
|
- sdkmanager --verbose --version
|
||||||
|
- yes | sdkmanager --licenses || true
|
||||||
|
- sdkmanager --verbose "platforms;android-${ANDROID_COMPILE_SDK}" "platform-tools" "build-tools;${ANDROID_BUILD_TOOLS}" || true
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
- echo "sdk.dir=${ANDROID_SDK_ROOT}" > local.properties
|
||||||
|
|
||||||
build:
|
assembleDebug:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- ./gradlew assembleDebug
|
- ./gradlew assembleDebug
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- app/build/outputs/apk/debug/*.apk
|
- app/build/outputs/**/*.apk
|
||||||
expire_in: 1 week
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
|
||||||
|
|
||||||
test:
|
debugTests:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- ./gradlew test
|
- ./gradlew -Pci --console=plain :app:testDebug
|
||||||
artifacts:
|
artifacts:
|
||||||
|
when: always
|
||||||
|
expire_in: 7 days
|
||||||
paths:
|
paths:
|
||||||
- app/build/test-results/test/*.xml
|
- app/build/reports/tests/testDebug/
|
||||||
- app/build/reports/tests/test/*.html
|
- app/build/test-results/testDebug/
|
||||||
expire_in: 1 week
|
- app/build/outputs/unit_test_code_coverage/debugUnitTest/
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
||||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
|
||||||
|
|||||||
+14
-11
@@ -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 {
|
||||||
|
|||||||
+4
@@ -6,6 +6,7 @@ import androidx.room.Query
|
|||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import com.prodhack.moscow2025.data.base.BasePaginationDAO
|
import com.prodhack.moscow2025.data.base.BasePaginationDAO
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
||||||
@@ -18,4 +19,7 @@ interface ResumeDao: BasePaginationDAO<ResumeEntity> {
|
|||||||
|
|
||||||
@Query("SELECT * FROM resumes")
|
@Query("SELECT * FROM resumes")
|
||||||
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
|
override fun getPaginatedData(): PagingSource<Int, ResumeEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM resumes WHERE id = :resumeId LIMIT 1")
|
||||||
|
fun getById(resumeId: String): Flow<ResumeEntity?>
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -34,7 +34,10 @@ data class ResumeEntity(
|
|||||||
about = aboutMe,
|
about = aboutMe,
|
||||||
experienceType = ExperienceType.valueOf(experienceType),
|
experienceType = ExperienceType.valueOf(experienceType),
|
||||||
skills = keySkills.split("|"),
|
skills = keySkills.split("|"),
|
||||||
prediction = Pair(fromSalary, toSalary),
|
prediction = if (fromSalary == null && toSalary == null) null else Pair(
|
||||||
|
fromSalary,
|
||||||
|
toSalary
|
||||||
|
),
|
||||||
recommendedSkills = recommendedSkills.split("|"),
|
recommendedSkills = recommendedSkills.split("|"),
|
||||||
city = city,
|
city = city,
|
||||||
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
||||||
|
|||||||
@@ -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() }),
|
||||||
@@ -98,7 +102,7 @@ data class ResumeDTO(
|
|||||||
data class ExperienceDTO(
|
data class ExperienceDTO(
|
||||||
val place: String,
|
val place: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
@SerialName("month_duration")
|
@SerialName("months_duration")
|
||||||
val monthDuration: Int,
|
val monthDuration: Int,
|
||||||
) {
|
) {
|
||||||
fun mapToDomain(): WorkExperience = WorkExperience(
|
fun mapToDomain(): WorkExperience = WorkExperience(
|
||||||
@@ -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")
|
||||||
|
|||||||
+2
@@ -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
|
||||||
|
|
||||||
|
|||||||
+28
-1
@@ -5,13 +5,16 @@ import com.prodhack.moscow2025.data.base.BaseRepository
|
|||||||
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
|
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
|
||||||
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
|
import com.prodhack.moscow2025.data.data_providers.local_db.AppDatabase
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
|
import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
|
||||||
|
import com.prodhack.moscow2025.data.dto.ResumeDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeIdDTO
|
import com.prodhack.moscow2025.data.dto.ResumeIdDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeListDTO
|
import com.prodhack.moscow2025.data.dto.ResumeListDTO
|
||||||
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
|
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
|
||||||
|
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
|
||||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||||
|
import com.prodhack.moscow2025.domain.utils.NetworkError
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
@@ -20,6 +23,9 @@ import io.ktor.http.contentType
|
|||||||
import io.ktor.http.parameters
|
import io.ktor.http.parameters
|
||||||
import io.ktor.http.path
|
import io.ktor.http.path
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
@@ -64,7 +70,28 @@ 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 }
|
||||||
|
|
||||||
|
override fun getResume(resumeId: String): Flow<Result<ResumeModel>> =
|
||||||
|
merge(
|
||||||
|
resumeDao.getById(resumeId = resumeId).map { entity ->
|
||||||
|
entity?.let { Result.success(it.mapToDomain()) }
|
||||||
|
?: Result.failure(NetworkError.InputError("Резюме не найдено"))
|
||||||
|
},
|
||||||
|
flow {
|
||||||
|
emit(networkRequest<ResumeDTO> {
|
||||||
|
method = HttpMethod.Get
|
||||||
|
url {
|
||||||
|
path("resume", resumeId)
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
it.also {
|
||||||
|
resumeDao.upsertAll(listOf(it.mapToDB()))
|
||||||
|
}.mapToDomain()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+3
@@ -3,10 +3,13 @@ package com.prodhack.moscow2025.domain.interfaces.resumes
|
|||||||
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface ResumeRepository {
|
interface ResumeRepository {
|
||||||
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
||||||
|
|
||||||
suspend fun suggestSkills(query: String): Result<List<String>>
|
suspend fun suggestSkills(query: String): Result<List<String>>
|
||||||
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
|
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
|
||||||
|
|
||||||
|
fun getResume(resumeId: String): Flow<Result<ResumeModel>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
+2
-3
@@ -1,14 +1,13 @@
|
|||||||
package com.prodhack.moscow2025.domain.usecase.auth
|
package com.prodhack.moscow2025.domain.usecase.auth
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import com.prodhack.moscow2025.domain.models.AuthField
|
import com.prodhack.moscow2025.domain.models.AuthField
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
import com.prodhack.moscow2025.domain.models.ExperienceType
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
import com.prodhack.moscow2025.domain.models.PhoneNumberPattern
|
import com.prodhack.moscow2025.domain.models.PhoneNumberPattern
|
||||||
import com.prodhack.moscow2025.domain.models.Project
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeField
|
import com.prodhack.moscow2025.domain.models.ResumeField
|
||||||
import com.prodhack.moscow2025.domain.models.WorkExperience
|
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||||
import com.prodhack.moscow2025.presentation.screens.createResume.UIEducation
|
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
data class ValidationResult<T>(
|
data class ValidationResult<T>(
|
||||||
@@ -94,7 +93,7 @@ class ValidateFieldsUseCase {
|
|||||||
keySkills: List<String>,
|
keySkills: List<String>,
|
||||||
city: String,
|
city: String,
|
||||||
workExperience: List<WorkExperience>,
|
workExperience: List<WorkExperience>,
|
||||||
education: List<UIEducation>,
|
education: List<Education>,
|
||||||
projects: List<Project>
|
projects: List<Project>
|
||||||
): ValidationResult<ResumeField> {
|
): ValidationResult<ResumeField> {
|
||||||
val errors = buildMap {
|
val errors = buildMap {
|
||||||
|
|||||||
+14
@@ -0,0 +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(
|
||||||
|
private val resumeRepository: ResumeRepository
|
||||||
|
) {
|
||||||
|
operator fun invoke(resumeId: String): Flow<Result<ResumeModel>> =
|
||||||
|
resumeRepository.getResume(resumeId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||||
|
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
|
@Single
|
||||||
|
class LoadHistoryUseCase {
|
||||||
|
|
||||||
|
}
|
||||||
+29
-29
@@ -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")
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,7 @@
|
|||||||
package com.prodhack.moscow2025.presentation.dataModels
|
package com.prodhack.moscow2025.presentation.dataModels
|
||||||
|
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
|
||||||
|
|
||||||
data class UIResumeBaseInfo(
|
data class UIResumeBaseInfo(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -11,7 +12,5 @@ data class UIResumeBaseInfo(
|
|||||||
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
|
fun ResumeModel.mapToBaseUIInfo(): UIResumeBaseInfo = UIResumeBaseInfo(
|
||||||
id = id,
|
id = id,
|
||||||
positionName = position,
|
positionName = position,
|
||||||
salary = prediction.first?.let { from ->
|
salary = prediction.toSalaryRangeString()
|
||||||
prediction.second?.let { to -> "$from-$to" } ?: from.toString()
|
|
||||||
} ?: prediction.second?.toString() ?: "Ошибка"
|
|
||||||
)
|
)
|
||||||
@@ -118,7 +118,7 @@ fun TTasksNavHost(
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable(AppDestination.ResumeCreation.route) {
|
composable(AppDestination.ResumeCreation.route) {
|
||||||
CreateResumeScreen()
|
CreateResumeScreen({ navController.popBackStack() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+286
-234
@@ -2,6 +2,7 @@ package com.prodhack.moscow2025.presentation.screens.createResume
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -15,7 +16,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
|
||||||
@@ -29,6 +30,10 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
|
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeField
|
import com.prodhack.moscow2025.domain.models.ResumeField
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
@@ -37,12 +42,14 @@ import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithD
|
|||||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch
|
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithSearch
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateResumeScreen(
|
fun CreateResumeScreen(
|
||||||
|
goBack: () -> Unit,
|
||||||
viewModel: CreateResumeViewModel = koinViewModel()
|
viewModel: CreateResumeViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -64,7 +71,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"
|
||||||
@@ -106,7 +114,7 @@ fun CreateResumeScreen(
|
|||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
TTTextFieldWithDropdown(
|
TTTextFieldWithDropdown(
|
||||||
value = formState.value.experience?.friendlyName ?: "",
|
value = formState.value.experience?.toReadableText() ?: "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
maxLines = Int.MAX_VALUE,
|
maxLines = Int.MAX_VALUE,
|
||||||
@@ -114,7 +122,7 @@ fun CreateResumeScreen(
|
|||||||
error = formState.value.errors[ResumeField.Experience],
|
error = formState.value.errors[ResumeField.Experience],
|
||||||
dropdownItems = viewModel.experienceOptions,
|
dropdownItems = viewModel.experienceOptions,
|
||||||
dropDownItem = {
|
dropDownItem = {
|
||||||
Text(text = it.friendlyName, style = typography.titleMedium, fontSize = 16.sp)
|
Text(text = it.toReadableText(), style = typography.labelLarge, fontSize = 16.sp)
|
||||||
},
|
},
|
||||||
onDropdownItemSelected = viewModel::onExperienceSelect
|
onDropdownItemSelected = viewModel::onExperienceSelect
|
||||||
)
|
)
|
||||||
@@ -179,244 +187,90 @@ fun CreateResumeScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
Text(
|
SectionCard(title = "Подробнее о вашем опыте работы:") {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
formState.value.workExperience.forEachIndexed { index, workExp ->
|
||||||
text = "Подробнее о вашем опыте работы:",
|
WorkExperienceForm(
|
||||||
style = typography.titleMedium,
|
index = index,
|
||||||
fontSize = 20.sp,
|
workExp = workExp,
|
||||||
textAlign = TextAlign.Center
|
errors = formState.value.errors,
|
||||||
)
|
onPlaceChange = { viewModel.changeWorkExperiencePlace(index, it) },
|
||||||
|
onDescriptionChange = { viewModel.changeWorkExperienceDescription(index, it) },
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
onDurationChange = { viewModel.changeWorkExperienceMonthDuration(index, it) },
|
||||||
|
onRemove = { viewModel.removeExperience(index) }
|
||||||
|
|
||||||
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 = {
|
|
||||||
Text(
|
|
||||||
text = it.friendlyName,
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
)
|
||||||
},
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formState.value.education.isEmpty()) {
|
if (formState.value.workExperience.isEmpty()) {
|
||||||
Text(
|
EmptyStateText()
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
text = "Пока ничего нет",
|
}
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
AddItemButton(
|
||||||
modifier = Modifier
|
text = "Добавить",
|
||||||
.fillMaxWidth(),
|
onClick = viewModel::addNewExperience,
|
||||||
shape = Shapes.smallRoundedBox,
|
containerColor = colorScheme.onSecondary,
|
||||||
onClick = viewModel::addNewEducation,
|
contentColor = colorScheme.secondary
|
||||||
colors = ButtonColors(
|
)
|
||||||
containerColor = colorScheme.onSecondary,
|
}
|
||||||
contentColor = colorScheme.secondary,
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
disabledContainerColor = colorScheme.onSecondary,
|
|
||||||
disabledContentColor = colorScheme.secondary
|
SectionCard(title = "Ваше образование:") {
|
||||||
)
|
formState.value.education.forEachIndexed { index, education ->
|
||||||
) {
|
EducationForm(
|
||||||
Text(
|
index = index,
|
||||||
text = "Добавить",
|
education = education,
|
||||||
style = typography.labelLarge,
|
errors = formState.value.errors,
|
||||||
fontSize = 18.sp,
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.value.education.isEmpty()) {
|
||||||
|
EmptyStateText()
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
}
|
||||||
|
|
||||||
|
AddItemButton(
|
||||||
|
text = "Добавить",
|
||||||
|
onClick = viewModel::addNewEducation,
|
||||||
|
containerColor = colorScheme.onSecondary,
|
||||||
|
contentColor = colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
Text(
|
SectionCard(title = "Интересные проекты:") {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
formState.value.projects.forEachIndexed { index, project ->
|
||||||
text = "Интересные проекты:",
|
ProjectForm(
|
||||||
style = typography.titleMedium,
|
index = index,
|
||||||
fontSize = 20.sp,
|
project = project,
|
||||||
textAlign = TextAlign.Center
|
errors = formState.value.errors,
|
||||||
)
|
onNameChange = { viewModel.changeProjectName(index, it) },
|
||||||
|
onDescriptionChange = { viewModel.changeProjectDescription(index, it) },
|
||||||
|
onRemove = { viewModel.removeProject(index) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
if (formState.value.projects.isEmpty()) {
|
||||||
|
EmptyStateText()
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
}
|
||||||
|
|
||||||
formState.value.projects.forEachIndexed { index, project ->
|
AddItemButton(
|
||||||
Text(
|
text = "Добавить",
|
||||||
text = "№${index + 1}:",
|
onClick = viewModel::addNewProject,
|
||||||
style = typography.labelLarge,
|
containerColor = colorScheme.onSecondary,
|
||||||
fontSize = 18.sp
|
contentColor = colorScheme.secondary
|
||||||
)
|
)
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
BigButton(
|
BigButton(
|
||||||
@@ -429,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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+37
-94
@@ -21,81 +21,19 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import kotlin.collections.minus
|
|
||||||
|
|
||||||
data class ResumeFormState(
|
data class ResumeFormState(
|
||||||
val about: String = "",
|
val about: String = "",
|
||||||
val position: String = "",
|
val position: String = "",
|
||||||
val experience: UIExperienceCount? = null,
|
val experience: ExperienceType? = null,
|
||||||
val keySkills: Set<String> = emptySet(),
|
val keySkills: Set<String> = emptySet(),
|
||||||
val city: String = "",
|
val city: String = "",
|
||||||
val workExperience: List<WorkExperience> = emptyList(),
|
val workExperience: List<WorkExperience> = emptyList(),
|
||||||
val education: List<UIEducation> = emptyList(),
|
val education: List<Education> = emptyList(),
|
||||||
val projects: List<Project> = emptyList(),
|
val projects: List<Project> = emptyList(),
|
||||||
val errors: Map<ResumeField, String> = emptyMap()
|
val errors: Map<ResumeField, String> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class UIExperienceCount(val friendlyName: String) {
|
|
||||||
data object NoExperience : UIExperienceCount("Без опыта")
|
|
||||||
data object LessThan1 : UIExperienceCount("Меньше года")
|
|
||||||
data object Between1And3 : UIExperienceCount("От 1 до 3 лет")
|
|
||||||
data object Between3And6 : UIExperienceCount("От 3 до 6 лет")
|
|
||||||
data object MoreThan6 : UIExperienceCount("Более 6 лет")
|
|
||||||
|
|
||||||
fun mapToDomain(): ExperienceType =
|
|
||||||
when (this) {
|
|
||||||
is NoExperience -> ExperienceType.NoExperience
|
|
||||||
is LessThan1 -> ExperienceType.LessThan1
|
|
||||||
is Between1And3 -> ExperienceType.Between1And3
|
|
||||||
is Between3And6 -> ExperienceType.Between3And6
|
|
||||||
is MoreThan6 -> ExperienceType.MoreThan6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UIEducation(
|
|
||||||
val place: String,
|
|
||||||
val grade: UIEducationGrade,
|
|
||||||
val specialization: String,
|
|
||||||
val description: String
|
|
||||||
)
|
|
||||||
|
|
||||||
//основное общее образование — basic_general_education
|
|
||||||
//
|
|
||||||
//среднее общее образование — secondary_general_education
|
|
||||||
//
|
|
||||||
//среднее профессиональное образование — secondary_professional_education
|
|
||||||
//
|
|
||||||
//бакалавриат — bachelor
|
|
||||||
//
|
|
||||||
//специалитет — specialist
|
|
||||||
//
|
|
||||||
//магистратура — master
|
|
||||||
//
|
|
||||||
//подготовка кадров высшей квалификации (аспірантура, ординатура, докторантура) — postgraduate_studies
|
|
||||||
sealed class UIEducationGrade(val friendlyName: String) {
|
|
||||||
data object BasicGeneralEducation : UIEducationGrade("Общее")
|
|
||||||
data object SecondaryGeneralEducation : UIEducationGrade("Среднее")
|
|
||||||
data object SecondaryProfessionalEducation : UIEducationGrade("Средне-специальное")
|
|
||||||
data object Bachelor : UIEducationGrade("Бакалавриат")
|
|
||||||
data object Specialist : UIEducationGrade("Специалитет")
|
|
||||||
data object Master : UIEducationGrade("Магистратура")
|
|
||||||
|
|
||||||
data object PostgraduateStudies: UIEducationGrade("Аспирантура и выше")
|
|
||||||
|
|
||||||
data object Other: UIEducationGrade("Другое")
|
|
||||||
|
|
||||||
fun mapToDomain(): EducationGrades = when (this) {
|
|
||||||
BasicGeneralEducation -> EducationGrades.BasicGeneralEducation
|
|
||||||
SecondaryGeneralEducation -> EducationGrades.SecondaryGeneralEducation
|
|
||||||
SecondaryProfessionalEducation -> EducationGrades.SecondaryProfessionalEducation
|
|
||||||
Bachelor -> EducationGrades.Bachelor
|
|
||||||
Specialist -> EducationGrades.Specialist
|
|
||||||
Master -> EducationGrades.Master
|
|
||||||
PostgraduateStudies -> EducationGrades.PostgraduateStudies
|
|
||||||
Other -> EducationGrades.Other
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class CreateResumeViewModel(
|
class CreateResumeViewModel(
|
||||||
private val suggestSkillsUseCase: SuggestSkillsUseCase,
|
private val suggestSkillsUseCase: SuggestSkillsUseCase,
|
||||||
@@ -136,15 +74,9 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val experienceOptions = listOf(
|
val experienceOptions = ExperienceType.entries.toList()
|
||||||
UIExperienceCount.NoExperience,
|
|
||||||
UIExperienceCount.LessThan1,
|
|
||||||
UIExperienceCount.Between1And3,
|
|
||||||
UIExperienceCount.Between3And6,
|
|
||||||
UIExperienceCount.MoreThan6
|
|
||||||
)
|
|
||||||
|
|
||||||
fun onExperienceSelect(value: UIExperienceCount) {
|
fun onExperienceSelect(value: ExperienceType) {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
experience = value,
|
experience = value,
|
||||||
@@ -161,6 +93,7 @@ class CreateResumeViewModel(
|
|||||||
errors = it.errors - ResumeField.KeySkills
|
errors = it.errors - ResumeField.KeySkills
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
skillSearchQuery.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRemoveSkill(value: String) {
|
fun onRemoveSkill(value: String) {
|
||||||
@@ -248,21 +181,14 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
val educationGradeOptions = listOf(
|
val educationGradeOptions = EducationGrades.entries.toList()
|
||||||
UIEducationGrade.BasicGeneralEducation,
|
|
||||||
UIEducationGrade.SecondaryGeneralEducation,
|
|
||||||
UIEducationGrade.SecondaryProfessionalEducation,
|
|
||||||
UIEducationGrade.Bachelor,
|
|
||||||
UIEducationGrade.Specialist,
|
|
||||||
UIEducationGrade.Master
|
|
||||||
)
|
|
||||||
|
|
||||||
fun addNewEducation() {
|
fun addNewEducation() {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
education = it.education + UIEducation(
|
education = it.education + Education(
|
||||||
place = "",
|
place = "",
|
||||||
grade = UIEducationGrade.Specialist,
|
grade = EducationGrades.Specialist,
|
||||||
specialization = "",
|
specialization = "",
|
||||||
description = ""
|
description = ""
|
||||||
)
|
)
|
||||||
@@ -270,6 +196,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(
|
||||||
@@ -281,7 +220,7 @@ class CreateResumeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeEducationGrade(index: Int, value: UIEducationGrade) {
|
fun changeEducationGrade(index: Int, value: EducationGrades) {
|
||||||
_formStateFillResume.update {
|
_formStateFillResume.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
education = it.education.mapIndexed { ind, education ->
|
education = it.education.mapIndexed { ind, education ->
|
||||||
@@ -323,6 +262,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(
|
||||||
@@ -350,7 +300,7 @@ class CreateResumeViewModel(
|
|||||||
val validation = validateDataUseCase.validateResume(
|
val validation = validateDataUseCase.validateResume(
|
||||||
about = _formStateFillResume.value.about,
|
about = _formStateFillResume.value.about,
|
||||||
position = _formStateFillResume.value.position,
|
position = _formStateFillResume.value.position,
|
||||||
experience = _formStateFillResume.value.experience?.mapToDomain(),
|
experience = _formStateFillResume.value.experience,
|
||||||
keySkills = _formStateFillResume.value.keySkills.toList(),
|
keySkills = _formStateFillResume.value.keySkills.toList(),
|
||||||
city = _formStateFillResume.value.city,
|
city = _formStateFillResume.value.city,
|
||||||
workExperience = _formStateFillResume.value.workExperience,
|
workExperience = _formStateFillResume.value.workExperience,
|
||||||
@@ -371,17 +321,10 @@ class CreateResumeViewModel(
|
|||||||
position = position,
|
position = position,
|
||||||
about = about,
|
about = about,
|
||||||
skills = keySkills.toList(),
|
skills = keySkills.toList(),
|
||||||
experienceType = experience!!.mapToDomain(),
|
experienceType = experience!!,
|
||||||
city = city.ifBlank { null },
|
city = city,
|
||||||
experience = workExperience,
|
experience = workExperience,
|
||||||
education = education.map {
|
education = education,
|
||||||
Education(
|
|
||||||
place = it.place,
|
|
||||||
grade = it.grade.mapToDomain(),
|
|
||||||
specialization = it.specialization,
|
|
||||||
description = it.description
|
|
||||||
)
|
|
||||||
},
|
|
||||||
projects = projects
|
projects = projects
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ResumeDiffScreen(){
|
||||||
|
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.resumes.LoadHistoryUseCase
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import org.koin.android.annotation.KoinViewModel
|
||||||
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
|
@KoinViewModel
|
||||||
|
class ResumeDiffViewModel(
|
||||||
|
@Provided resumeId: String,
|
||||||
|
private val loadHistoryUseCase: LoadHistoryUseCase
|
||||||
|
): BaseViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
+349
-2
@@ -1,14 +1,58 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardColors
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.placeholders.ErrorPlaceholder
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.placeholders.LoadingPlaceholder
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResumeDetailsScreen(
|
fun ErrorCollectorScope.ResumeDetailsScreen(
|
||||||
navBackStackEntry: NavBackStackEntry,
|
navBackStackEntry: NavBackStackEntry,
|
||||||
viewModel: ResumeDetailsViewModel = koinViewModel {
|
viewModel: ResumeDetailsViewModel = koinViewModel {
|
||||||
parametersOf(
|
parametersOf(
|
||||||
@@ -16,7 +60,310 @@ fun ResumeDetailsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Text("Opened resume details for id ${navBackStackEntry.arguments?.getString(AppDestination.ResumeDetails.ARG_ID, "") ?: ""}")
|
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 мес."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+16
-1
@@ -1,11 +1,26 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
package com.prodhack.moscow2025.presentation.screens.resumeDetails
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Provided
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class ResumeDetailsViewModel(
|
class ResumeDetailsViewModel(
|
||||||
@Provided resumeId: String
|
@Provided resumeId: String,
|
||||||
|
private val getResumeInfoUseCase: GetResumeInfoUseCase
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
private val _resumeState = MutableUIStateFlow<ResumeModel>()
|
||||||
|
val resumeState: StateFlow<UIState<ResumeModel>> = _resumeState
|
||||||
|
|
||||||
|
fun loadResume(resumeId: String) {
|
||||||
|
getResumeInfoUseCase(resumeId).collectRequest(_resumeState)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadResume(resumeId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.prodhack.moscow2025.presentation.utils
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
|
|
||||||
|
fun ExperienceType.toReadableText(): String = when (this) {
|
||||||
|
ExperienceType.NoExperience -> "Нет опыта"
|
||||||
|
ExperienceType.LessThan1 -> "Меньше года"
|
||||||
|
ExperienceType.Between1And3 -> "1-3 года"
|
||||||
|
ExperienceType.Between3And6 -> "3-6 лет"
|
||||||
|
ExperienceType.MoreThan6 -> "Более 6 лет"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EducationGrades.toReadableText(): String = when (this) {
|
||||||
|
EducationGrades.BasicGeneralEducation -> "Базовое общее"
|
||||||
|
EducationGrades.SecondaryGeneralEducation -> "Среднее общее"
|
||||||
|
EducationGrades.SecondaryProfessionalEducation -> "Среднее профессиональное"
|
||||||
|
EducationGrades.Bachelor -> "Бакалавр"
|
||||||
|
EducationGrades.Specialist -> "Специалист"
|
||||||
|
EducationGrades.Master -> "Магистр"
|
||||||
|
EducationGrades.PostgraduateStudies -> "Аспирантура"
|
||||||
|
EducationGrades.Other -> "Другое"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Pair<Int?, Int?>?.toSalaryRangeString(): String = when {
|
||||||
|
this == null -> "Загрузка..."
|
||||||
|
first != null && second != null -> "${first}₽ - ${second}₽"
|
||||||
|
first != null -> "от ${first}₽"
|
||||||
|
second != null -> "до ${second}₽"
|
||||||
|
else -> "Ошибка"
|
||||||
|
}
|
||||||
@@ -25,4 +25,3 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "MoscowHackatonTemplate"
|
rootProject.name = "MoscowHackatonTemplate"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||
Reference in New Issue
Block a user