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