You've already forked RekomenciMobile
Merge branch 'resume_form'
This commit is contained in:
@@ -85,6 +85,7 @@ dependencies {
|
||||
implementation(libs.compose.animation.graphics)
|
||||
implementation(libs.material.icons.extended)
|
||||
implementation(libs.androidx.foundation)
|
||||
implementation(libs.androidx.runtime)
|
||||
androidTestImplementation(platform(libs.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.ui.tooling)
|
||||
|
||||
+27
-3
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "3e896e9a3d3b2f61149f8c0fde7e5964",
|
||||
"identityHash": "b16cf19ddaafa74ea796a48650e53014",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "users",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "resumes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `experience_type` TEXT NOT NULL, `about_me` TEXT NOT NULL, `key_skills` TEXT NOT NULL, `position` TEXT NOT NULL, `from_salary` INTEGER, `to_salary` INTEGER, `recommended_skills` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `experience_type` TEXT NOT NULL, `about_me` TEXT NOT NULL, `key_skills` TEXT NOT NULL, `position` TEXT NOT NULL, `from_salary` INTEGER, `to_salary` INTEGER, `recommended_skills` TEXT NOT NULL, `city` TEXT NOT NULL, `experience` TEXT NOT NULL, `education` TEXT NOT NULL, `projects` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -102,6 +102,30 @@
|
||||
"columnName": "recommended_skills",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "experience",
|
||||
"columnName": "experience",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "education",
|
||||
"columnName": "education",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "projects",
|
||||
"columnName": "projects",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -114,7 +138,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e896e9a3d3b2f61149f8c0fde7e5964')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b16cf19ddaafa74ea796a48650e53014')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package com.prodhack.moscow2025.data.data_providers.local_db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.CleanUpDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.ResumeDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.dao.UserDao
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.JsonTypeConverters
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
||||
|
||||
@@ -13,6 +15,7 @@ import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(JsonTypeConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UserDao
|
||||
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.prodhack.moscow2025.data.data_providers.local_db.entities
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.prodhack.moscow2025.domain.models.Education
|
||||
import com.prodhack.moscow2025.domain.models.Project
|
||||
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||
|
||||
object JsonTypeConverters {
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
@TypeConverter
|
||||
fun fromWorkExperienceList(value: List<WorkExperience>): String {
|
||||
val type = object : TypeToken<List<WorkExperience>>() {}.type
|
||||
return gson.toJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toWorkExperienceList(value: String): List<WorkExperience> {
|
||||
val type = object : TypeToken<List<WorkExperience>>() {}.type
|
||||
return gson.fromJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromEducationList(value: List<Education>): String {
|
||||
val type = object : TypeToken<List<Education>>() {}.type
|
||||
return gson.toJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toEducationList(value: String): List<Education> {
|
||||
val type = object : TypeToken<List<Education>>() {}.type
|
||||
return gson.fromJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromProjectList(value: List<Project>): String {
|
||||
val type = object : TypeToken<List<Project>>() {}.type
|
||||
return gson.toJson(value, type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toProjectList(value: String): List<Project> {
|
||||
val type = object : TypeToken<List<Project>>() {}.type
|
||||
return gson.fromJson(value, type)
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -5,7 +5,6 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import kotlin.math.exp
|
||||
|
||||
@Entity(tableName = "resumes")
|
||||
data class ResumeEntity(
|
||||
@@ -23,7 +22,11 @@ data class ResumeEntity(
|
||||
@ColumnInfo("to_salary")
|
||||
val toSalary: Int?,
|
||||
@ColumnInfo("recommended_skills")
|
||||
val recommendedSkills: String
|
||||
val recommendedSkills: String,
|
||||
val city: String,
|
||||
val experience: String, // Store as JSON string, requires TypeConverter
|
||||
val education: String, // Store as JSON string, requires TypeConverter
|
||||
val projects: String // Store as JSON string, requires TypeConverter
|
||||
) {
|
||||
fun mapToDomain(): ResumeModel = ResumeModel(
|
||||
id = id,
|
||||
@@ -32,6 +35,10 @@ data class ResumeEntity(
|
||||
experienceType = ExperienceType.valueOf(experienceType),
|
||||
skills = keySkills.split("|"),
|
||||
prediction = Pair(fromSalary, toSalary),
|
||||
recommendedSkills = recommendedSkills.split("|")
|
||||
recommendedSkills = recommendedSkills.split("|"),
|
||||
city = city,
|
||||
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
||||
education = JsonTypeConverters.toEducationList(education),
|
||||
projects = JsonTypeConverters.toProjectList(projects)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.prodhack.moscow2025.data.dto
|
||||
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.JsonTypeConverters
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||
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 kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -32,6 +37,14 @@ enum class ExperienceTypeDTO {
|
||||
}
|
||||
}
|
||||
|
||||
fun ExperienceType.mapToData(): ExperienceTypeDTO = when (this) {
|
||||
ExperienceType.NoExperience -> ExperienceTypeDTO.NoExperience
|
||||
ExperienceType.LessThan1 -> ExperienceTypeDTO.LessThan1
|
||||
ExperienceType.Between1And3 -> ExperienceTypeDTO.Between1And3
|
||||
ExperienceType.Between3And6 -> ExperienceTypeDTO.Between3And6
|
||||
ExperienceType.MoreThan6 -> ExperienceTypeDTO.MoreThan6
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResumeDTO(
|
||||
val id: String,
|
||||
@@ -42,6 +55,10 @@ data class ResumeDTO(
|
||||
@SerialName("key_skills")
|
||||
val keySkills: List<String>,
|
||||
val position: String,
|
||||
val city: String,
|
||||
val experience: List<ExperienceDTO>,
|
||||
val education: List<EducationDTO>,
|
||||
val project: List<ProjectDTO>,
|
||||
val prediction: PredictionDTO
|
||||
) {
|
||||
fun mapToDomain(): ResumeModel = ResumeModel(
|
||||
@@ -54,7 +71,11 @@ data class ResumeDTO(
|
||||
prediction.fromSalary.toIntOrNull(),
|
||||
prediction.toSalary.toIntOrNull()
|
||||
),
|
||||
recommendedSkills = prediction.recommendedSkills
|
||||
recommendedSkills = prediction.recommendedSkills,
|
||||
city = city,
|
||||
experience = experience.map { it.mapToDomain() },
|
||||
education = education.map { it.mapToDomain() },
|
||||
projects = project.map { it.mapToDomain() }
|
||||
)
|
||||
|
||||
fun mapToDB(): ResumeEntity = ResumeEntity(
|
||||
@@ -65,10 +86,107 @@ data class ResumeDTO(
|
||||
fromSalary = prediction.fromSalary.toIntOrNull(),
|
||||
toSalary = prediction.toSalary.toIntOrNull(),
|
||||
recommendedSkills = prediction.recommendedSkills.joinToString("|"),
|
||||
experienceType = experienceType.mapToDomain().name
|
||||
experienceType = experienceType.mapToDomain().name,
|
||||
city = city,
|
||||
experience = JsonTypeConverters.fromWorkExperienceList(experience.map { it.mapToDomain() }),
|
||||
education = JsonTypeConverters.fromEducationList(education.map { it.mapToDomain() }),
|
||||
projects = JsonTypeConverters.fromProjectList(project.map { it.mapToDomain() }),
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ExperienceDTO(
|
||||
val place: String,
|
||||
val description: String,
|
||||
@SerialName("month_duration")
|
||||
val monthDuration: Int,
|
||||
) {
|
||||
fun mapToDomain(): WorkExperience = WorkExperience(
|
||||
place = place,
|
||||
description = description,
|
||||
monthDuration = monthDuration
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EducationDTO(
|
||||
val place: String,
|
||||
val grade: EducationGradesDTO,
|
||||
val specialization: String,
|
||||
val description: String
|
||||
) {
|
||||
fun mapToDomain(): Education = Education(
|
||||
place = place,
|
||||
grade = grade.mapToDomain(),
|
||||
specialization = specialization,
|
||||
description = description
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class EducationGradesDTO {
|
||||
@SerialName("basic_general_education")
|
||||
BasicGeneralEducation,
|
||||
|
||||
@SerialName("secondary_general_education")
|
||||
SecondaryGeneralEducation,
|
||||
|
||||
@SerialName("secondary_professional_education")
|
||||
SecondaryProfessionalEducation,
|
||||
|
||||
@SerialName("bachelor")
|
||||
Bachelor,
|
||||
|
||||
@SerialName("specialist")
|
||||
Specialist,
|
||||
|
||||
@SerialName("master")
|
||||
Master,
|
||||
|
||||
@SerialName("postgraduate_studies")
|
||||
PostgraduateStudies,
|
||||
|
||||
@SerialName("other")
|
||||
Other;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ProjectDTO(
|
||||
val name: String,
|
||||
val description: String
|
||||
) {
|
||||
fun mapToDomain(): Project = Project(
|
||||
name = name,
|
||||
description = description
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResumeCreateDTO(
|
||||
@SerialName("experience_type")
|
||||
val experienceType: ExperienceTypeDTO,
|
||||
@SerialName("about_me")
|
||||
val aboutMe: String,
|
||||
@SerialName("key_skills")
|
||||
val keySkills: List<String>,
|
||||
val position: String,
|
||||
val city: String,
|
||||
val experience: List<ExperienceDTO>,
|
||||
val education: List<EducationDTO>,
|
||||
val project: List<ProjectDTO>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PredictionDTO(
|
||||
@SerialName("from_salary")
|
||||
@@ -83,3 +201,14 @@ data class PredictionDTO(
|
||||
data class ResumeListDTO(
|
||||
val resumes: List<ResumeDTO>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResumeIdDTO(
|
||||
@SerialName("resume_id")
|
||||
val resumeId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResumeSkillDTO(
|
||||
val name: String
|
||||
)
|
||||
+30
@@ -4,12 +4,21 @@ import androidx.paging.map
|
||||
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.ResumeIdDTO
|
||||
import com.prodhack.moscow2025.data.dto.ResumeListDTO
|
||||
import com.prodhack.moscow2025.data.dto.ResumeSkillDTO
|
||||
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 io.ktor.client.request.setBody
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.parameters
|
||||
import io.ktor.http.path
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@@ -37,4 +46,25 @@ class ResumeRepositoryImpl(
|
||||
}.map { it -> it.resumes.map { it.mapToDB() } }
|
||||
}
|
||||
).map { it -> it.map { it.mapToDomain() } }
|
||||
|
||||
override suspend fun suggestSkills(query: String): Result<List<String>> =
|
||||
networkRequest<List<ResumeSkillDTO>> {
|
||||
method = HttpMethod.Get
|
||||
url {
|
||||
path("key_skills")
|
||||
parameters.append("query", query)
|
||||
}
|
||||
}.map { it.map { it.name } }
|
||||
|
||||
override suspend fun createResume(resumeForm: ResumeCreationModel): Result<String> =
|
||||
networkRequest<ResumeIdDTO> {
|
||||
method = HttpMethod.Post
|
||||
|
||||
url {
|
||||
url("/resume")
|
||||
}
|
||||
|
||||
setBody(ResumeCreateDTO)
|
||||
contentType(ContentType.Application.Json)
|
||||
}.map { it.resumeId }
|
||||
}
|
||||
+4
@@ -1,8 +1,12 @@
|
||||
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
|
||||
|
||||
interface ResumeRepository {
|
||||
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
||||
|
||||
suspend fun suggestSkills(query: String): Result<List<String>>
|
||||
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
|
||||
}
|
||||
|
||||
@@ -9,3 +9,12 @@ data class LoginData(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
enum class AuthField {
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
Password,
|
||||
ConfirmPassword,
|
||||
Phone
|
||||
}
|
||||
@@ -5,11 +5,55 @@ data class ResumeModel(
|
||||
val position: String,
|
||||
val about: String,
|
||||
val skills: List<String>,
|
||||
val city: String,
|
||||
val experienceType: ExperienceType,
|
||||
val experience: List<WorkExperience>,
|
||||
val education: List<Education>,
|
||||
val projects: List<Project>,
|
||||
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 experienceType: ExperienceType,
|
||||
val experience: List<WorkExperience>,
|
||||
val education: List<Education>,
|
||||
val projects: List<Project>
|
||||
)
|
||||
|
||||
data class WorkExperience(
|
||||
val place: String,
|
||||
val description: String,
|
||||
val monthDuration: Int?
|
||||
)
|
||||
|
||||
data class Education(
|
||||
val place: String,
|
||||
val grade: EducationGrades,
|
||||
val specialization: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
enum class EducationGrades {
|
||||
BasicGeneralEducation,
|
||||
SecondaryGeneralEducation,
|
||||
SecondaryProfessionalEducation,
|
||||
Bachelor,
|
||||
Specialist,
|
||||
Master,
|
||||
PostgraduateStudies,
|
||||
Other
|
||||
}
|
||||
|
||||
data class Project(
|
||||
val name: String,
|
||||
val description: String
|
||||
)
|
||||
|
||||
enum class ExperienceType {
|
||||
NoExperience,
|
||||
LessThan1,
|
||||
@@ -17,3 +61,32 @@ enum class ExperienceType {
|
||||
Between3And6,
|
||||
MoreThan6
|
||||
}
|
||||
|
||||
sealed class ResumeField {
|
||||
data object About : ResumeField()
|
||||
data object Position : ResumeField()
|
||||
data object Experience : ResumeField()
|
||||
data object KeySkills : ResumeField()
|
||||
|
||||
data object City : ResumeField()
|
||||
|
||||
data class WorkExperiencePlace(val id: Int) : ResumeField()
|
||||
|
||||
data class WorkExperienceDescription(val id: Int) : ResumeField()
|
||||
|
||||
data class WorkExperienceMonthDuration(val id: Int) : ResumeField()
|
||||
|
||||
|
||||
data class EducationPlace(val id: Int) : ResumeField()
|
||||
|
||||
data class EducationGrade(val id: Int) : ResumeField()
|
||||
|
||||
data class EducationSpecialization(val id: Int) : ResumeField()
|
||||
|
||||
data class EducationDescription(val id: Int) : ResumeField()
|
||||
|
||||
data class ProjectName(val id: Int) : ResumeField()
|
||||
|
||||
data class ProjectDescription(val id: Int) : ResumeField()
|
||||
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.auth
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import com.prodhack.moscow2025.domain.models.PhoneNumberPattern
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
enum class AuthField {
|
||||
FirstName,
|
||||
LastName,
|
||||
Email,
|
||||
Password,
|
||||
ConfirmPassword,
|
||||
Phone
|
||||
}
|
||||
|
||||
|
||||
data class ValidationResult(
|
||||
val errors: Map<AuthField, String> = emptyMap()
|
||||
) {
|
||||
val isValid: Boolean
|
||||
get() = errors.isEmpty()
|
||||
}
|
||||
|
||||
@Single
|
||||
class ValidateAuthFieldsUseCase {
|
||||
fun validateProfile(
|
||||
chosenPattern: PhoneNumberPattern?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
phone: String
|
||||
): ValidationResult {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||
AuthField.Phone,
|
||||
"Некорректный номер телефона"
|
||||
)
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateFillProfile(
|
||||
chosenPattern: PhoneNumberPattern?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phone: String
|
||||
): ValidationResult {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||
AuthField.Phone,
|
||||
"Некорректный номер телефона"
|
||||
)
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateSignUp(
|
||||
email: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
): ValidationResult {
|
||||
val errors = buildMap {
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
validatePassword(password)?.let { put(AuthField.Password, it) }
|
||||
if (confirmPassword.isBlank()) put(AuthField.ConfirmPassword, "Повторите пароль")
|
||||
|
||||
if (password != confirmPassword) {
|
||||
put(AuthField.ConfirmPassword, "Пароли не совпадают")
|
||||
}
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validatePassword(password: String): String? {
|
||||
if (password.length < 8) {
|
||||
return "Пароль должен быть не менее 8 символов"
|
||||
}
|
||||
if (!password.any { it.isUpperCase() }) {
|
||||
return "Пароль должен содержать хотя бы одну заглавную букву"
|
||||
}
|
||||
if (!password.any { it.isDigit() }) {
|
||||
return "Пароль должен содержать хотя бы одну цифру"
|
||||
}
|
||||
if (!password.any { !it.isLetterOrDigit() }) {
|
||||
return "Пароль должен содержать хотя бы один специальный символ"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun validateLogin(
|
||||
email: String,
|
||||
password: String
|
||||
): ValidationResult {
|
||||
val errors = buildMap {
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
validatePassword(password)?.let { put(AuthField.Password, it) }
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateProfile(
|
||||
firstName: String,
|
||||
secondName: String,
|
||||
): ValidationResult {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (secondName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
private fun isEmailValid(email: String): Boolean =
|
||||
email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
|
||||
|
||||
private fun isPhoneValid(phone: String): Boolean =
|
||||
Patterns.PHONE.matcher(phone).matches()
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
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.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>(
|
||||
val errors: Map<T, String> = emptyMap()
|
||||
) {
|
||||
val isValid: Boolean
|
||||
get() = errors.isEmpty()
|
||||
}
|
||||
|
||||
@Single
|
||||
class ValidateFieldsUseCase {
|
||||
fun validateProfile(
|
||||
chosenPattern: PhoneNumberPattern?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
email: String,
|
||||
phone: String
|
||||
): ValidationResult<AuthField> {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||
AuthField.Phone,
|
||||
"Некорректный номер телефона"
|
||||
)
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateFillProfile(
|
||||
chosenPattern: PhoneNumberPattern?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
phone: String
|
||||
): ValidationResult<AuthField> {
|
||||
val errors = buildMap {
|
||||
if (firstName.isBlank()) put(AuthField.FirstName, "Введите имя")
|
||||
if (lastName.isBlank()) put(AuthField.LastName, "Введите фамилию")
|
||||
val maxCount = chosenPattern!!.pattern.count { it == '0' }
|
||||
if (phone.isNotBlank() && !isPhoneValid(phone) && phone.length != maxCount) put(
|
||||
AuthField.Phone,
|
||||
"Некорректный номер телефона"
|
||||
)
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateSignUp(
|
||||
email: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
): ValidationResult<AuthField> {
|
||||
val errors = buildMap {
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
validatePassword(password)?.let { put(AuthField.Password, it) }
|
||||
if (confirmPassword.isBlank()) put(AuthField.ConfirmPassword, "Повторите пароль")
|
||||
|
||||
if (password != confirmPassword) {
|
||||
put(AuthField.ConfirmPassword, "Пароли не совпадают")
|
||||
}
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validateLogin(
|
||||
email: String,
|
||||
password: String
|
||||
): ValidationResult<AuthField> {
|
||||
val errors = buildMap {
|
||||
if (!isEmailValid(email)) put(AuthField.Email, "Некорректный email")
|
||||
validatePassword(password)?.let { put(AuthField.Password, it) }
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
|
||||
fun validateResume(
|
||||
about: String,
|
||||
position: String,
|
||||
experience: ExperienceType?,
|
||||
keySkills: List<String>,
|
||||
city: String,
|
||||
workExperience: List<WorkExperience>,
|
||||
education: List<UIEducation>,
|
||||
projects: List<Project>
|
||||
): ValidationResult<ResumeField> {
|
||||
val errors = buildMap {
|
||||
if (about.isBlank()) put(ResumeField.About, "Без этого мы не сможем рассчитать вашу ЗП")
|
||||
if (position.isBlank()) put(
|
||||
ResumeField.Position,
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (experience == null) put(
|
||||
ResumeField.Experience,
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (keySkills.isEmpty()) put(ResumeField.KeySkills, "Укажите хотя бы один навык")
|
||||
|
||||
if (city.isEmpty()) put(ResumeField.City, "Без этого мы не сможем рассчитать вашу ЗП")
|
||||
workExperience.forEachIndexed { index, exp ->
|
||||
if (exp.place.isBlank()) put(
|
||||
ResumeField.WorkExperiencePlace(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (exp.description.isBlank()) put(
|
||||
ResumeField.WorkExperienceDescription(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (exp.monthDuration == null) put(
|
||||
ResumeField.WorkExperienceMonthDuration(index),
|
||||
"Введите корректное число"
|
||||
)
|
||||
}
|
||||
|
||||
education.forEachIndexed { index, educ ->
|
||||
if (educ.place.isBlank()) put(
|
||||
ResumeField.EducationPlace(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (educ.description.isBlank()) put(
|
||||
ResumeField.EducationDescription(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (educ.specialization.isBlank()) put(
|
||||
ResumeField.EducationSpecialization(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
}
|
||||
|
||||
projects.forEachIndexed { index, prj ->
|
||||
if (prj.name.isBlank()) put(
|
||||
ResumeField.ProjectName(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
if (prj.description.isBlank()) put(
|
||||
ResumeField.ProjectDescription(index),
|
||||
"Без этого мы не сможем рассчитать вашу ЗП"
|
||||
)
|
||||
}
|
||||
}
|
||||
return ValidationResult(errors)
|
||||
}
|
||||
|
||||
fun validatePassword(password: String): String? {
|
||||
if (password.length < 8) {
|
||||
return "Пароль должен быть не менее 8 символов"
|
||||
}
|
||||
if (!password.any { it.isUpperCase() }) {
|
||||
return "Пароль должен содержать хотя бы одну заглавную букву"
|
||||
}
|
||||
if (!password.any { it.isDigit() }) {
|
||||
return "Пароль должен содержать хотя бы одну цифру"
|
||||
}
|
||||
if (!password.any { !it.isLetterOrDigit() }) {
|
||||
return "Пароль должен содержать хотя бы один специальный символ"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isEmailValid(email: String): Boolean =
|
||||
email.isNotBlank() && Patterns.EMAIL_ADDRESS.matcher(email).matches()
|
||||
|
||||
private fun isPhoneValid(phone: String): Boolean =
|
||||
Patterns.PHONE.matcher(phone).matches()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||
|
||||
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@Single
|
||||
class CreateResumeUseCase(
|
||||
private val resumeRepository: ResumeRepository
|
||||
) {
|
||||
suspend operator fun invoke(resumeForm: ResumeCreationModel): Result<String> =
|
||||
resumeRepository.createResume(resumeForm)
|
||||
}
|
||||
+4
@@ -30,6 +30,10 @@ class LoadResumeListUseCase(private val resumeRepository: ResumeRepository) {
|
||||
"Ktor"
|
||||
),
|
||||
experienceType = ExperienceType.Between3And6,
|
||||
city = "Moscow",
|
||||
experience = listOf(),
|
||||
education = listOf(),
|
||||
projects = listOf(),
|
||||
prediction = Pair(200000, 230000),
|
||||
recommendedSkills = listOf("KMP")
|
||||
)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||
|
||||
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
|
||||
@Single
|
||||
class SuggestSkillsUseCase(
|
||||
private val resumeRepository: ResumeRepository
|
||||
) {
|
||||
suspend operator fun invoke(query: String): Result<List<String>> =
|
||||
resumeRepository.suggestSkills(query = query)
|
||||
|
||||
// mock
|
||||
// suspend operator fun invoke(query: String): Result<List<String>> =
|
||||
// Result.success(listOf(
|
||||
// "Python", "Kotlin", "Java", "C#", "JavaScript",
|
||||
// "TypeScript", "Go", "Rust", "Swift", "PHP",
|
||||
// "Ruby", "C++", "Dart", "HTML", "CSS",
|
||||
// "SQL", "NoSQL", "MongoDB", "PostgreSQL", "MySQL",
|
||||
// "Docker", "Kubernetes", "AWS", "Azure", "Google Cloud Platform",
|
||||
// "React", "Angular", "Vue.js", "Node.js", "Spring Boot",
|
||||
// "Django", "Flask", "ASP.NET", "Ruby on Rails", "Laravel",
|
||||
// "Android", "iOS", "Flutter", "React Native", "Xamarin",
|
||||
// "Git", "Jira", "Confluence", "Jenkins", "Travis CI",
|
||||
// "Agile", "Scrum", "Kanban", "DevOps", "GraphQL"
|
||||
// ).filter { it.contains(query, ignoreCase = true) }.take(10))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.prodhack.moscow2025.presentation.components.standart
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.theme.Shapes
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||
|
||||
@Composable
|
||||
fun TBubble(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
isSelected: Boolean = false,
|
||||
onDelete: (() -> Unit)? = null
|
||||
) {
|
||||
val shapes = MaterialTheme.shapes
|
||||
val typography = MaterialTheme.typography
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val bubbleColor =
|
||||
animateColorAsState(
|
||||
if (isSelected) colorScheme.primary else colorScheme.secondary
|
||||
)
|
||||
val bubbleBorderColor =
|
||||
animateColorAsState(
|
||||
if (isSelected) colorScheme.primaryFixed else colorScheme.secondaryFixed
|
||||
)
|
||||
val contentColor =
|
||||
animateColorAsState(
|
||||
if (isSelected) colorScheme.onPrimary else colorScheme.onSecondary
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = bubbleColor.value,
|
||||
shape = shapes.small
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = bubbleBorderColor.value,
|
||||
shape = shapes.small
|
||||
)
|
||||
.clip(Shapes.smallRoundedBox)
|
||||
.padding(horizontal = Paddings.small, vertical = Paddings.verySmall),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = typography.labelLarge,
|
||||
fontSize = 16.sp,
|
||||
color = contentColor.value
|
||||
)
|
||||
onDelete?.let {
|
||||
Spacer(modifier = Modifier.width(Paddings.verySmall))
|
||||
Icon(
|
||||
modifier = Modifier.noRippleClickable(it),
|
||||
painter = painterResource(R.drawable.ic_remove),
|
||||
tint = contentColor.value,
|
||||
contentDescription = "Remove"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -62,7 +62,7 @@ fun TTTextField(
|
||||
val typography = MaterialTheme.typography
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
FieldWrapper(modifier = modifier) {
|
||||
FieldWrapper(modifier = modifier, disableHeightLimit = singleLine.not()) {
|
||||
OutlinedTextField(
|
||||
modifier = textFieldModifier
|
||||
.fillMaxWidth()
|
||||
@@ -266,7 +266,7 @@ fun <T> TTTextFieldWithSearch(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit = {},
|
||||
readOnly: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
label: String,
|
||||
error: String? = null,
|
||||
singleLine: Boolean = true,
|
||||
@@ -368,9 +368,13 @@ fun <T> TTTextFieldWithSearch(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FieldWrapper(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
fun FieldWrapper(
|
||||
modifier: Modifier = Modifier,
|
||||
disableHeightLimit: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier.height(70.dp),
|
||||
modifier.then(if (disableHeightLimit) Modifier else Modifier.height(70.dp)),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
|
||||
@@ -20,5 +20,7 @@ sealed class AppDestination(val route: String) {
|
||||
data object ResumeDetails : AppDestination("resume/details") {
|
||||
const val ARG_ID = "id"
|
||||
}
|
||||
|
||||
data object ResumeCreation: AppDestination("resume/creation")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.prodhack.moscow2025.presentation.screens.main.MainScreen
|
||||
import com.prodhack.moscow2025.domain.utils.NetworkError
|
||||
import com.prodhack.moscow2025.presentation.screens.createResume.CreateResumeScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.fillProfile.FillProfileScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.login.LoginScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen
|
||||
@@ -91,11 +92,15 @@ fun TTasksNavHost(
|
||||
}
|
||||
|
||||
composable(AppDestination.Main.route) {
|
||||
MainScreen(openResumeDetails = { id ->
|
||||
MainScreen(
|
||||
openResumeDetails = { id ->
|
||||
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
|
||||
putString(AppDestination.ResumeDetails.ARG_ID, id)
|
||||
})
|
||||
})
|
||||
}, openCreateResume = {
|
||||
navController.navigate(AppDestination.ResumeCreation.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(AppDestination.Profile.route)
|
||||
@@ -111,6 +116,10 @@ fun TTasksNavHost(
|
||||
composable(AppDestination.ResumeDetails.route) {
|
||||
ResumeDetailsScreen(navBackStackEntry = it)
|
||||
}
|
||||
|
||||
composable(AppDestination.ResumeCreation.route) {
|
||||
CreateResumeScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.createResume
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.layout.width
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.ResumeField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextFieldWithDropdown
|
||||
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.ui.noRippleClickable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
fun CreateResumeScreen(
|
||||
viewModel: CreateResumeViewModel = koinViewModel()
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val typography = MaterialTheme.typography
|
||||
|
||||
val formState = viewModel.formStateFillResume.collectAsState()
|
||||
|
||||
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
|
||||
.rotate(180f)
|
||||
.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_arr_details),
|
||||
tint = colorScheme.onBackground,
|
||||
contentDescription = "go back"
|
||||
)
|
||||
Text(text = "Новое резюме", style = typography.titleLarge, fontSize = 24.sp)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
TTTextField(
|
||||
value = formState.value.position,
|
||||
onValueChange = viewModel::onPositionChange,
|
||||
label = "Какая должность вас интересует?",
|
||||
error = formState.value.errors[ResumeField.Position]
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
TTTextField(
|
||||
value = formState.value.city,
|
||||
onValueChange = viewModel::onCityChange,
|
||||
label = "Ваш город",
|
||||
error = formState.value.errors[ResumeField.City]
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
TTTextField(
|
||||
value = formState.value.about,
|
||||
onValueChange = viewModel::onAboutChange,
|
||||
singleLine = false,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
label = "Расскажите о себе",
|
||||
error = formState.value.errors[ResumeField.Position]
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
TTTextFieldWithDropdown(
|
||||
value = formState.value.experience?.friendlyName ?: "",
|
||||
onValueChange = {},
|
||||
singleLine = false,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
label = "Какой у вас опыт в данной сфере?",
|
||||
error = formState.value.errors[ResumeField.Experience],
|
||||
dropdownItems = viewModel.experienceOptions,
|
||||
dropDownItem = {
|
||||
Text(text = it.friendlyName, style = typography.titleMedium, fontSize = 16.sp)
|
||||
},
|
||||
onDropdownItemSelected = viewModel::onExperienceSelect
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
TTTextFieldWithSearch(
|
||||
value = viewModel.skillSearchQuery.value,
|
||||
onValueChange = {
|
||||
viewModel.skillSearchQuery.value = it
|
||||
},
|
||||
label = "Ваши навыки",
|
||||
error = formState.value.errors[ResumeField.Experience],
|
||||
dropdownItems = viewModel.suggestedSkills.collectAsState(emptyList()).value,
|
||||
dropDownItem = {
|
||||
Text(text = it, style = typography.labelLarge, fontSize = 16.sp)
|
||||
},
|
||||
onDropdownItemSelected = viewModel::onAddSkill,
|
||||
trailingIcon = {
|
||||
if (viewModel.skillSearchQuery.value.isNotBlank()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
viewModel.onAddSkill(viewModel.skillSearchQuery.value)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"Добавить",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 12.sp,
|
||||
color = colorScheme.onPrimary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(Paddings.verySmall))
|
||||
Icon(
|
||||
modifier = Modifier.size(12.dp),
|
||||
painter = painterResource(R.drawable.ic_plus),
|
||||
tint = colorScheme.onPrimary,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(Paddings.medium))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
Paddings.small
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
Paddings.small
|
||||
)
|
||||
) {
|
||||
formState.value.keySkills.forEach { skillName ->
|
||||
TBubble(text = skillName) {
|
||||
viewModel.onRemoveSkill(skillName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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
|
||||
)
|
||||
},
|
||||
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()) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Пока ничего нет",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
shape = Shapes.smallRoundedBox,
|
||||
onClick = viewModel::addNewEducation,
|
||||
colors = ButtonColors(
|
||||
containerColor = colorScheme.onSecondary,
|
||||
contentColor = colorScheme.secondary,
|
||||
disabledContainerColor = colorScheme.onSecondary,
|
||||
disabledContentColor = colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Интересные проекты:",
|
||||
style = typography.titleMedium,
|
||||
fontSize = 20.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
formState.value.projects.forEachIndexed { index, project ->
|
||||
Text(
|
||||
text = "№${index + 1}:",
|
||||
style = typography.labelLarge,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||
|
||||
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))
|
||||
BigButton(
|
||||
onClick = viewModel::submit,
|
||||
buttonText = "Узнать свою ЗП",
|
||||
isLoading = viewModel.resumeFillState.collectAsState().value.isLoading
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.createResume
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.ResumeField
|
||||
import com.prodhack.moscow2025.domain.models.WorkExperience
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.resumes.CreateResumeUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.resumes.SuggestSkillsUseCase
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
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 keySkills: Set<String> = emptySet(),
|
||||
val city: String = "",
|
||||
val workExperience: List<WorkExperience> = emptyList(),
|
||||
val education: List<UIEducation> = 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,
|
||||
private val validateDataUseCase: ValidateFieldsUseCase,
|
||||
private val createResumeUseCase: CreateResumeUseCase
|
||||
) : BaseViewModel() {
|
||||
private val _formStateFillResume = MutableStateFlow(ResumeFormState())
|
||||
val formStateFillResume: StateFlow<ResumeFormState> = _formStateFillResume
|
||||
|
||||
private val _resumeFillState = MutableUIStateFlow<String>()
|
||||
val resumeFillState: StateFlow<UIState<String>> = _resumeFillState
|
||||
|
||||
// Simple fields
|
||||
fun onAboutChange(value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
about = value,
|
||||
errors = it.errors - ResumeField.About
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPositionChange(value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
position = value,
|
||||
errors = it.errors - ResumeField.Position
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onCityChange(value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
city = value,
|
||||
errors = it.errors - ResumeField.City
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val experienceOptions = listOf(
|
||||
UIExperienceCount.NoExperience,
|
||||
UIExperienceCount.LessThan1,
|
||||
UIExperienceCount.Between1And3,
|
||||
UIExperienceCount.Between3And6,
|
||||
UIExperienceCount.MoreThan6
|
||||
)
|
||||
|
||||
fun onExperienceSelect(value: UIExperienceCount) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
experience = value,
|
||||
errors = it.errors - ResumeField.Experience
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Skills
|
||||
fun onAddSkill(value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
keySkills = it.keySkills + value,
|
||||
errors = it.errors - ResumeField.KeySkills
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRemoveSkill(value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
keySkills = it.keySkills - value,
|
||||
errors = it.errors - ResumeField.KeySkills
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val skillSearchQuery = mutableStateOf("")
|
||||
|
||||
val suggestedSkills = snapshotFlow { skillSearchQuery.value }.map {
|
||||
suggestSkillsUseCase(it).getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
// Experience work
|
||||
|
||||
fun addNewExperience() {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
workExperience = it.workExperience + WorkExperience("", "", null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeExperience(id: Int) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
workExperience = it.workExperience.filterIndexed { index, _ -> index != id },
|
||||
errors = it.errors
|
||||
- ResumeField.WorkExperienceDescription(id)
|
||||
- ResumeField.WorkExperienceMonthDuration(id)
|
||||
- ResumeField.WorkExperiencePlace(id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeWorkExperiencePlace(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
workExperience = it.workExperience.mapIndexed { ind, experience ->
|
||||
if (ind == index) experience.copy(
|
||||
place = value
|
||||
) else experience
|
||||
},
|
||||
errors = it.errors
|
||||
- ResumeField.WorkExperiencePlace(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeWorkExperienceMonthDuration(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
workExperience = it.workExperience.mapIndexed { ind, experience ->
|
||||
if (ind == index) {
|
||||
value.toIntOrNull()?.let {
|
||||
experience.copy(
|
||||
monthDuration = it
|
||||
)
|
||||
}
|
||||
?: if (value.isEmpty()) experience.copy(monthDuration = null) else experience
|
||||
} else experience
|
||||
},
|
||||
errors = it.errors
|
||||
- ResumeField.WorkExperienceDescription(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeWorkExperienceDescription(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
workExperience = it.workExperience.mapIndexed { ind, experience ->
|
||||
if (ind == index) experience.copy(
|
||||
description = value
|
||||
) else experience
|
||||
},
|
||||
errors = it.errors
|
||||
- ResumeField.WorkExperienceDescription(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Education
|
||||
val educationGradeOptions = listOf(
|
||||
UIEducationGrade.BasicGeneralEducation,
|
||||
UIEducationGrade.SecondaryGeneralEducation,
|
||||
UIEducationGrade.SecondaryProfessionalEducation,
|
||||
UIEducationGrade.Bachelor,
|
||||
UIEducationGrade.Specialist,
|
||||
UIEducationGrade.Master
|
||||
)
|
||||
|
||||
fun addNewEducation() {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education + UIEducation(
|
||||
place = "",
|
||||
grade = UIEducationGrade.Specialist,
|
||||
specialization = "",
|
||||
description = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeEducationPlace(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education.mapIndexed { ind, education ->
|
||||
if (ind == index) education.copy(place = value) else education
|
||||
},
|
||||
errors = it.errors - ResumeField.EducationPlace(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeEducationGrade(index: Int, value: UIEducationGrade) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education.mapIndexed { ind, education ->
|
||||
if (ind == index) education.copy(grade = value) else education
|
||||
},
|
||||
errors = it.errors - ResumeField.EducationGrade(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeEducationSpecialization(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education.mapIndexed { ind, education ->
|
||||
if (ind == index) education.copy(specialization = value) else education
|
||||
},
|
||||
errors = it.errors - ResumeField.EducationSpecialization(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeEducationDescription(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
education = it.education.mapIndexed { ind, education ->
|
||||
if (ind == index) education.copy(description = value) else education
|
||||
},
|
||||
errors = it.errors - ResumeField.EducationDescription(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Projects
|
||||
fun addNewProject() {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
projects = it.projects + Project("", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeProjectName(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
projects = it.projects.mapIndexed { ind, project ->
|
||||
if (ind == index) project.copy(name = value) else project
|
||||
},
|
||||
errors = it.errors - ResumeField.ProjectName(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeProjectDescription(index: Int, value: String) {
|
||||
_formStateFillResume.update {
|
||||
it.copy(
|
||||
projects = it.projects.mapIndexed { ind, project ->
|
||||
if (ind == index) project.copy(description = value) else project
|
||||
},
|
||||
errors = it.errors - ResumeField.ProjectDescription(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun submit() {
|
||||
viewModelScope.launch {
|
||||
val validation = validateDataUseCase.validateResume(
|
||||
about = _formStateFillResume.value.about,
|
||||
position = _formStateFillResume.value.position,
|
||||
experience = _formStateFillResume.value.experience?.mapToDomain(),
|
||||
keySkills = _formStateFillResume.value.keySkills.toList(),
|
||||
city = _formStateFillResume.value.city,
|
||||
workExperience = _formStateFillResume.value.workExperience,
|
||||
education = _formStateFillResume.value.education,
|
||||
projects = _formStateFillResume.value.projects
|
||||
)
|
||||
|
||||
if (!validation.isValid) {
|
||||
_formStateFillResume.update { it.copy(errors = validation.errors) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
_resumeFillState.emit(UIState.Loading())
|
||||
|
||||
val result = createResumeUseCase(
|
||||
with(_formStateFillResume.value) {
|
||||
ResumeCreationModel(
|
||||
position = position,
|
||||
about = about,
|
||||
skills = keySkills.toList(),
|
||||
experienceType = experience!!.mapToDomain(),
|
||||
city = city.ifBlank { null },
|
||||
experience = workExperience,
|
||||
education = education.map {
|
||||
Education(
|
||||
place = it.place,
|
||||
grade = it.grade.mapToDomain(),
|
||||
specialization = it.specialization,
|
||||
description = it.description
|
||||
)
|
||||
},
|
||||
projects = projects
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
result.collectRequest(_resumeFillState)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-20
@@ -1,35 +1,24 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.fillProfile
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
@@ -43,25 +32,17 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
+4
-4
@@ -14,11 +14,11 @@ import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
|
||||
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
||||
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import com.prodhack.moscow2025.presentation.utils.convertNumberToPattern
|
||||
@@ -65,7 +65,7 @@ data class FillProfileFormState(
|
||||
@KoinViewModel
|
||||
class FillProfileViewModel(
|
||||
private val updateUserUseCase: UpdateUserUseCase,
|
||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||
private val validateFieldsUseCase: ValidateFieldsUseCase,
|
||||
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||
private val galleryRepository: GalleryRepository
|
||||
) : BaseViewModel() {
|
||||
@@ -168,7 +168,7 @@ class FillProfileViewModel(
|
||||
|
||||
fun submit() {
|
||||
viewModelScope.launch {
|
||||
val validation = validateAuthFieldsUseCase.validateFillProfile(
|
||||
val validation = validateFieldsUseCase.validateFillProfile(
|
||||
firstName = _formStateFillProfile.value.firstName,
|
||||
lastName = _formStateFillProfile.value.lastName,
|
||||
phone = _formStateFillProfile.value.phone,
|
||||
|
||||
@@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.prodhack.moscow2025.R
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.login
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.LoginData
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.LoginUserUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -22,7 +22,7 @@ data class LoginFormState(
|
||||
@KoinViewModel
|
||||
class LoginViewModel(
|
||||
private val loginUserUseCase: LoginUserUseCase,
|
||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase
|
||||
private val validateFieldsUseCase: ValidateFieldsUseCase
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _formState = MutableStateFlow(LoginFormState())
|
||||
@@ -41,7 +41,7 @@ class LoginViewModel(
|
||||
|
||||
fun submit() {
|
||||
viewModelScope.launch {
|
||||
val validation = validateAuthFieldsUseCase.validateLogin(
|
||||
val validation = validateFieldsUseCase.validateLogin(
|
||||
email = _formState.value.email,
|
||||
password = _formState.value.password
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
fun ErrorCollectorScope.MainScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
openResumeDetails: (String) -> Unit,
|
||||
openCreateResume: () -> Unit,
|
||||
viewModel: MainScreenViewModel = koinViewModel()
|
||||
) {
|
||||
val typography = MaterialTheme.typography
|
||||
@@ -53,7 +54,7 @@ fun ErrorCollectorScope.MainScreen(
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp),
|
||||
.padding(horizontal = Paddings.large),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TopLogo()
|
||||
@@ -78,9 +79,13 @@ fun ErrorCollectorScope.MainScreen(
|
||||
color = colorScheme.onBackground
|
||||
)
|
||||
|
||||
BigButton(onClick = {
|
||||
BigButton(
|
||||
onClick = {
|
||||
TODO()
|
||||
}, buttonText = "Создать резюме", isLoading = false)
|
||||
},
|
||||
buttonText = "Создать резюме",
|
||||
isLoading = false
|
||||
)
|
||||
} else if (items.loadState.hasError) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
@@ -123,7 +128,7 @@ fun ErrorCollectorScope.MainScreen(
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = Paddings.medium),
|
||||
onClick = {
|
||||
Toast.makeText(context, "Will be soon...", Toast.LENGTH_SHORT).show()
|
||||
openCreateResume()
|
||||
},
|
||||
text = "Добавить резюме"
|
||||
)
|
||||
|
||||
-1
@@ -2,7 +2,6 @@ package com.prodhack.moscow2025.presentation.screens.main
|
||||
|
||||
import androidx.paging.map
|
||||
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeListUseCase
|
||||
import com.prodhack.moscow2025.presentation.dataModels.UIResumeBaseInfo
|
||||
import com.prodhack.moscow2025.presentation.dataModels.mapToBaseUIInfo
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
+1
-15
@@ -1,14 +1,9 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.profile
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -17,16 +12,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
@@ -40,24 +31,19 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.FieldWrapper
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TPhoneCountryList
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TPhoneField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.theme.Shapes
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||
import com.prodhack.moscow2025.presentation.utils.PhoneVisualTransformation
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.showSnackbar
|
||||
|
||||
+9
-13
@@ -6,8 +6,6 @@ import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -16,13 +14,13 @@ import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.prodhack.moscow2025.data.data_providers.PhoneNumberPatternsProvider
|
||||
import com.prodhack.moscow2025.domain.interfaces.GalleryRepository
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.UpdateUserData
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.GetUserUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.LogOutUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.UpdateUserUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.GetDefaultPhoneNumberPatternUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
|
||||
import com.prodhack.moscow2025.presentation.screens.fillProfile.UIPhoneNumberPattern
|
||||
import com.prodhack.moscow2025.presentation.screens.fillProfile.mapToUI
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
@@ -39,7 +37,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
|
||||
data class ProfileState(
|
||||
data class ProfileFormState(
|
||||
val email: String = "",
|
||||
val firstName: String = "",
|
||||
val lastName: String = "",
|
||||
@@ -51,7 +49,7 @@ data class ProfileState(
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ProfileState
|
||||
other as ProfileFormState
|
||||
|
||||
if (email != other.email) return false
|
||||
if (firstName != other.firstName) return false
|
||||
@@ -72,21 +70,19 @@ data class ProfileState(
|
||||
result = 31 * result + errors.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@KoinViewModel
|
||||
class ProfileScreenViewModel(
|
||||
private val getUserUseCase: GetUserUseCase,
|
||||
private val updateUserUseCase: UpdateUserUseCase,
|
||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase,
|
||||
private val validateFieldsUseCase: ValidateFieldsUseCase,
|
||||
private val logOutUseCase: LogOutUseCase,
|
||||
private val getDefaultPhoneNumberPatternUseCase: GetDefaultPhoneNumberPatternUseCase,
|
||||
galleryRepository: GalleryRepository
|
||||
) : BaseViewModel() {
|
||||
private val _formStateProfile = MutableStateFlow(ProfileState())
|
||||
val formStateFillProfile: StateFlow<ProfileState> = _formStateProfile
|
||||
private val _formStateProfile = MutableStateFlow(ProfileFormState())
|
||||
val formStateFillProfile: StateFlow<ProfileFormState> = _formStateProfile
|
||||
|
||||
private val _profileState = MutableUIStateFlow<String>()
|
||||
val profileState: StateFlow<UIState<String>> = _profileState
|
||||
@@ -203,7 +199,7 @@ class ProfileScreenViewModel(
|
||||
fun submit() {
|
||||
viewModelScope.launch {
|
||||
val pattern = chosenPattern.value
|
||||
val validation = validateAuthFieldsUseCase.validateProfile(
|
||||
val validation = validateFieldsUseCase.validateProfile(
|
||||
chosenPattern = pattern?.mapToDomain(),
|
||||
firstName = _formStateProfile.value.firstName,
|
||||
lastName = _formStateProfile.value.lastName,
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.prodhack.moscow2025.R
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTPasswordField
|
||||
import com.prodhack.moscow2025.presentation.components.standart.TTTextField
|
||||
|
||||
+4
-31
@@ -1,10 +1,10 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.register
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.prodhack.moscow2025.domain.models.AuthField
|
||||
import com.prodhack.moscow2025.domain.models.RegisterData
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.AuthField
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.RegisterUserUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateAuthFieldsUseCase
|
||||
import com.prodhack.moscow2025.domain.usecase.auth.ValidateFieldsUseCase
|
||||
import com.prodhack.moscow2025.presentation.utils.UIState
|
||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -23,7 +23,7 @@ data class RegisterFormState(
|
||||
@KoinViewModel
|
||||
class RegisterViewModel(
|
||||
private val registerUserUseCase: RegisterUserUseCase,
|
||||
private val validateAuthFieldsUseCase: ValidateAuthFieldsUseCase
|
||||
private val validateFieldsUseCase: ValidateFieldsUseCase
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _formStateSignUp = MutableStateFlow(RegisterFormState())
|
||||
@@ -58,8 +58,7 @@ class RegisterViewModel(
|
||||
|
||||
fun submit() {
|
||||
viewModelScope.launch {
|
||||
|
||||
val validation = validateAuthFieldsUseCase.validateSignUp(
|
||||
val validation = validateFieldsUseCase.validateSignUp(
|
||||
email = _formStateSignUp.value.email,
|
||||
password = _formStateSignUp.value.password,
|
||||
confirmPassword = _formStateSignUp.value.confirmPassword
|
||||
@@ -79,32 +78,6 @@ class RegisterViewModel(
|
||||
)
|
||||
)
|
||||
result.collectRequest(_registerState)
|
||||
|
||||
// val validation = validateAuthFieldsUseCase.validateRegister(
|
||||
// firstName = _formStateSignUp.value.firstName,
|
||||
// lastName = _formStateSignUp.value.lastName,
|
||||
// email = _formStateSignUp.value.email,
|
||||
// password = _formStateSignUp.value.password,
|
||||
// confirmPassword = _formStateSignUp.value.confirmPassword,
|
||||
// phone = _formStateSignUp.value.ph
|
||||
// )
|
||||
//
|
||||
// if (!validation.isValid) {
|
||||
// _formStateSignUp.update { it.copy(errors = validation.errors) }
|
||||
// return@launch
|
||||
// }
|
||||
//
|
||||
// _registerState.emit(UIState.Loading())
|
||||
//
|
||||
// val result = registerUserUseCase(
|
||||
// RegisterData(
|
||||
// firstName = _formStateSignUp.value.firstName,
|
||||
// secondName = _formStateSignUp.value.lastName,
|
||||
// email = _formStateSignUp.value.email,
|
||||
// password = _formStateSignUp.value.password
|
||||
// )
|
||||
// )
|
||||
// result.collectRequest(_registerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
sealed class UIState<T> {
|
||||
sealed class UIState<T>() {
|
||||
class Idle<T> : UIState<T>()
|
||||
class Loading<T> : UIState<T>()
|
||||
class Error<T>(val error: NetworkError) : UIState<T>()
|
||||
@@ -43,6 +43,9 @@ sealed class UIState<T> {
|
||||
|
||||
val isSuccess: Boolean
|
||||
get() = this is Success
|
||||
|
||||
val isLoading: Boolean
|
||||
get() = this is Loading
|
||||
}
|
||||
|
||||
interface ErrorCallbacks {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.31,2.25H13.69C13.907,2.25 14.096,2.25 14.274,2.278C14.621,2.334 14.95,2.469 15.234,2.675C15.519,2.88 15.752,3.15 15.914,3.461C15.998,3.621 16.057,3.8 16.126,4.005L16.237,4.34L16.267,4.425C16.358,4.676 16.526,4.892 16.748,5.04C16.97,5.189 17.233,5.262 17.5,5.25H20.5C20.699,5.25 20.89,5.329 21.03,5.47C21.171,5.61 21.25,5.801 21.25,6C21.25,6.199 21.171,6.39 21.03,6.53C20.89,6.671 20.699,6.75 20.5,6.75H3.5C3.301,6.75 3.11,6.671 2.97,6.53C2.829,6.39 2.75,6.199 2.75,6C2.75,5.801 2.829,5.61 2.97,5.47C3.11,5.329 3.301,5.25 3.5,5.25H6.59C6.857,5.244 7.115,5.152 7.326,4.988C7.537,4.824 7.69,4.597 7.763,4.34L7.875,4.005C7.943,3.8 8.002,3.621 8.085,3.461C8.247,3.149 8.48,2.88 8.765,2.675C9.05,2.469 9.379,2.333 9.726,2.278C9.904,2.25 10.093,2.25 10.309,2.25M9.007,5.25C9.076,5.112 9.135,4.969 9.182,4.822L9.282,4.522C9.373,4.249 9.394,4.194 9.415,4.154C9.469,4.05 9.547,3.96 9.642,3.892C9.737,3.823 9.846,3.778 9.962,3.759C10.092,3.747 10.223,3.744 10.354,3.75H13.644C13.932,3.75 13.992,3.752 14.036,3.76C14.152,3.778 14.261,3.824 14.356,3.892C14.451,3.961 14.529,4.05 14.583,4.154C14.604,4.194 14.625,4.249 14.716,4.523L14.816,4.823L14.855,4.935C14.894,5.044 14.94,5.149 14.991,5.25H9.007Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M5.915,8.45C5.902,8.251 5.81,8.066 5.66,7.935C5.511,7.804 5.315,7.738 5.116,7.751C4.918,7.765 4.733,7.856 4.602,8.006C4.471,8.156 4.405,8.351 4.418,8.55L4.882,15.502C4.967,16.784 5.036,17.82 5.198,18.634C5.367,19.479 5.653,20.185 6.245,20.738C6.837,21.291 7.56,21.531 8.415,21.642C9.237,21.75 10.275,21.75 11.561,21.75H12.44C13.725,21.75 14.764,21.75 15.586,21.642C16.44,21.531 17.164,21.292 17.756,20.738C18.347,20.185 18.633,19.478 18.802,18.634C18.964,17.821 19.032,16.784 19.118,15.502L19.582,8.55C19.595,8.351 19.529,8.156 19.398,8.006C19.267,7.856 19.082,7.765 18.883,7.751C18.685,7.738 18.489,7.804 18.34,7.935C18.19,8.066 18.098,8.251 18.085,8.45L17.625,15.35C17.535,16.697 17.471,17.635 17.331,18.34C17.194,19.025 17.004,19.387 16.731,19.643C16.457,19.899 16.083,20.065 15.391,20.155C14.678,20.248 13.738,20.25 12.387,20.25H11.613C10.263,20.25 9.323,20.248 8.609,20.155C7.917,20.065 7.543,19.899 7.269,19.643C6.996,19.387 6.806,19.025 6.669,18.341C6.529,17.635 6.465,16.697 6.375,15.349L5.915,8.45Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M9.425,10.254C9.623,10.234 9.82,10.294 9.974,10.42C10.128,10.545 10.226,10.727 10.246,10.925L10.746,15.925C10.761,16.12 10.698,16.313 10.573,16.463C10.447,16.613 10.268,16.708 10.073,16.727C9.878,16.747 9.684,16.69 9.531,16.568C9.378,16.446 9.278,16.269 9.254,16.075L8.754,11.075C8.734,10.877 8.794,10.679 8.92,10.526C9.045,10.372 9.227,10.274 9.425,10.254ZM14.575,10.254C14.773,10.274 14.954,10.372 15.08,10.525C15.206,10.679 15.266,10.876 15.246,11.074L14.746,16.074C14.721,16.268 14.622,16.444 14.469,16.566C14.316,16.687 14.122,16.744 13.927,16.725C13.733,16.706 13.554,16.611 13.428,16.462C13.302,16.312 13.24,16.12 13.254,15.925L13.754,10.925C13.774,10.727 13.872,10.546 14.025,10.42C14.179,10.294 14.377,10.234 14.575,10.254Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
@@ -39,6 +39,7 @@ googleServicesGMC = "4.4.4"
|
||||
crashlytics = "3.0.6"
|
||||
foundation = "1.9.4"
|
||||
kotzilla = "1.4.0"
|
||||
runtime = "1.9.5"
|
||||
|
||||
|
||||
[libraries]
|
||||
@@ -116,6 +117,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
|
||||
compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
|
||||
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
|
||||
kotzilla-sdk = { group = "io.kotzilla", name = "kotzilla-sdk", version.ref = "kotzilla" }
|
||||
androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
||||
|
||||
|
||||
[plugins]
|
||||
|
||||
Reference in New Issue
Block a user