You've already forked RekomenciMobile
some fixes
This commit is contained in:
+10
-16
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "153c4dcbf8d785dbfcd495eee39ae220",
|
"identityHash": "aac4b458e39f7bddd2a666a7b0645eb7",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "users",
|
"tableName": "users",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "resumes",
|
"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, `city` TEXT NOT NULL, `experience` TEXT NOT NULL, `education` TEXT NOT NULL, `projects` 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` REAL, `to_salary` REAL, `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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -90,12 +90,12 @@
|
|||||||
{
|
{
|
||||||
"fieldPath": "fromSalary",
|
"fieldPath": "fromSalary",
|
||||||
"columnName": "from_salary",
|
"columnName": "from_salary",
|
||||||
"affinity": "INTEGER"
|
"affinity": "REAL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "toSalary",
|
"fieldPath": "toSalary",
|
||||||
"columnName": "to_salary",
|
"columnName": "to_salary",
|
||||||
"affinity": "INTEGER"
|
"affinity": "REAL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "recommendedSkills",
|
"fieldPath": "recommendedSkills",
|
||||||
@@ -137,14 +137,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "resume_history",
|
"tableName": "resume_history",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resume_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)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resume_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` REAL, `to_salary` REAL, `recommended_skills` TEXT NOT NULL, `city` TEXT NOT NULL, `experience` TEXT NOT NULL, `education` TEXT NOT NULL, `projects` TEXT NOT NULL, PRIMARY KEY(`resume_id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldPath": "resumeId",
|
"fieldPath": "resumeId",
|
||||||
"columnName": "resume_id",
|
"columnName": "resume_id",
|
||||||
@@ -178,12 +172,12 @@
|
|||||||
{
|
{
|
||||||
"fieldPath": "fromSalary",
|
"fieldPath": "fromSalary",
|
||||||
"columnName": "from_salary",
|
"columnName": "from_salary",
|
||||||
"affinity": "INTEGER"
|
"affinity": "REAL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "toSalary",
|
"fieldPath": "toSalary",
|
||||||
"columnName": "to_salary",
|
"columnName": "to_salary",
|
||||||
"affinity": "INTEGER"
|
"affinity": "REAL"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "recommendedSkills",
|
"fieldPath": "recommendedSkills",
|
||||||
@@ -217,16 +211,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
"autoGenerate": true,
|
"autoGenerate": false,
|
||||||
"columnNames": [
|
"columnNames": [
|
||||||
"id"
|
"resume_id"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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, '153c4dcbf8d785dbfcd495eee39ae220')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aac4b458e39f7bddd2a666a7b0645eb7')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -18,9 +18,9 @@ data class ResumeEntity(
|
|||||||
val keySkills: String,
|
val keySkills: String,
|
||||||
val position: String,
|
val position: String,
|
||||||
@ColumnInfo("from_salary")
|
@ColumnInfo("from_salary")
|
||||||
val fromSalary: Int?,
|
val fromSalary: Float?,
|
||||||
@ColumnInfo("to_salary")
|
@ColumnInfo("to_salary")
|
||||||
val toSalary: Int?,
|
val toSalary: Float?,
|
||||||
@ColumnInfo("recommended_skills")
|
@ColumnInfo("recommended_skills")
|
||||||
val recommendedSkills: String,
|
val recommendedSkills: String,
|
||||||
val city: String,
|
val city: String,
|
||||||
|
|||||||
+3
-4
@@ -8,8 +8,7 @@ import com.prodhack.moscow2025.domain.models.ResumeModel
|
|||||||
|
|
||||||
@Entity(tableName = "resume_history")
|
@Entity(tableName = "resume_history")
|
||||||
data class ResumeHistoryEntity(
|
data class ResumeHistoryEntity(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = false)
|
||||||
val id: Long = 0,
|
|
||||||
@ColumnInfo("resume_id")
|
@ColumnInfo("resume_id")
|
||||||
val resumeId: String,
|
val resumeId: String,
|
||||||
@ColumnInfo("experience_type")
|
@ColumnInfo("experience_type")
|
||||||
@@ -20,9 +19,9 @@ data class ResumeHistoryEntity(
|
|||||||
val keySkills: String,
|
val keySkills: String,
|
||||||
val position: String,
|
val position: String,
|
||||||
@ColumnInfo("from_salary")
|
@ColumnInfo("from_salary")
|
||||||
val fromSalary: Int?,
|
val fromSalary: Float?,
|
||||||
@ColumnInfo("to_salary")
|
@ColumnInfo("to_salary")
|
||||||
val toSalary: Int?,
|
val toSalary: Float?,
|
||||||
@ColumnInfo("recommended_skills")
|
@ColumnInfo("recommended_skills")
|
||||||
val recommendedSkills: String,
|
val recommendedSkills: String,
|
||||||
val city: String,
|
val city: String,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ data class ResumeDTO(
|
|||||||
val city: String,
|
val city: String,
|
||||||
val experience: List<ExperienceDTO> = emptyList(),
|
val experience: List<ExperienceDTO> = emptyList(),
|
||||||
val education: List<EducationDTO> = emptyList(),
|
val education: List<EducationDTO> = emptyList(),
|
||||||
|
@SerialName("projects")
|
||||||
val project: List<ProjectDTO> = emptyList(),
|
val project: List<ProjectDTO> = emptyList(),
|
||||||
val prediction: PredictionDTO? = null
|
val prediction: PredictionDTO? = null
|
||||||
) {
|
) {
|
||||||
@@ -71,8 +72,8 @@ data class ResumeDTO(
|
|||||||
experienceType = experienceType.mapToDomain(),
|
experienceType = experienceType.mapToDomain(),
|
||||||
prediction = prediction?.let {
|
prediction = prediction?.let {
|
||||||
Pair(
|
Pair(
|
||||||
it.fromSalary.toIntOrNull(),
|
it.fromSalary.toFloatOrNull(),
|
||||||
it.toSalary.toIntOrNull()
|
it.toSalary.toFloatOrNull()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
recommendedSkills = prediction?.recommendedSkills,
|
recommendedSkills = prediction?.recommendedSkills,
|
||||||
@@ -87,8 +88,8 @@ data class ResumeDTO(
|
|||||||
aboutMe = aboutMe,
|
aboutMe = aboutMe,
|
||||||
keySkills = keySkills.joinToString("|"),
|
keySkills = keySkills.joinToString("|"),
|
||||||
position = position,
|
position = position,
|
||||||
fromSalary = prediction?.fromSalary?.toIntOrNull(),
|
fromSalary = prediction?.fromSalary?.toFloatOrNull(),
|
||||||
toSalary = prediction?.toSalary?.toIntOrNull(),
|
toSalary = prediction?.toSalary?.toFloatOrNull(),
|
||||||
recommendedSkills = prediction?.recommendedSkills?.joinToString("|") ?: "",
|
recommendedSkills = prediction?.recommendedSkills?.joinToString("|") ?: "",
|
||||||
experienceType = experienceType.mapToDomain().name,
|
experienceType = experienceType.mapToDomain().name,
|
||||||
city = city,
|
city = city,
|
||||||
@@ -219,6 +220,7 @@ data class ResumeCreateDTO(
|
|||||||
val city: String,
|
val city: String,
|
||||||
val experience: List<ExperienceDTO>? = null,
|
val experience: List<ExperienceDTO>? = null,
|
||||||
val education: List<EducationDTO>? = null,
|
val education: List<EducationDTO>? = null,
|
||||||
|
@SerialName("projects")
|
||||||
val project: List<ProjectDTO>? = null,
|
val project: List<ProjectDTO>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ data class ResumeModel(
|
|||||||
val experience: List<WorkExperience>,
|
val experience: List<WorkExperience>,
|
||||||
val education: List<Education>,
|
val education: List<Education>,
|
||||||
val projects: List<Project>,
|
val projects: List<Project>,
|
||||||
val prediction: Pair<Int?, Int?>?,
|
val prediction: Pair<Float?, Float?>?,
|
||||||
val recommendedSkills: List<String>?
|
val recommendedSkills: List<String>?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,4 +30,9 @@ sealed class AppDestination(val route: String) {
|
|||||||
data object ResumeEdit : AppDestination("resume/edit") {
|
data object ResumeEdit : AppDestination("resume/edit") {
|
||||||
const val ARG_ID = "id"
|
const val ARG_ID = "id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data object ResumeDiff : AppDestination("resume/diff") {
|
||||||
|
const val ARG_FIRST = "first_version"
|
||||||
|
const val ARG_SECOND = "second_version"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
|
|||||||
import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen
|
import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen
|
||||||
import com.prodhack.moscow2025.presentation.screens.editResume.EditResumeScreen
|
import com.prodhack.moscow2025.presentation.screens.editResume.EditResumeScreen
|
||||||
import com.prodhack.moscow2025.presentation.screens.resumeHistory.ResumeHistoryScreen
|
import com.prodhack.moscow2025.presentation.screens.resumeHistory.ResumeHistoryScreen
|
||||||
|
import com.prodhack.moscow2025.presentation.screens.diffScreen.ResumeDiffScreen
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
|
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||||
|
|
||||||
@@ -140,6 +141,10 @@ fun TTasksNavHost(
|
|||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(AppDestination.ResumeDiff.route) {
|
||||||
|
ResumeDiffScreen(navBackStackEntry = it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-1
@@ -1,8 +1,170 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
||||||
|
|
||||||
|
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.material3.Card
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.resumes.CalculateResumeDiffUseCase
|
||||||
|
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.toSalaryRangeString
|
||||||
|
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResumeDiffScreen(){
|
fun ErrorCollectorScope.ResumeDiffScreen(
|
||||||
|
navBackStackEntry: NavBackStackEntry,
|
||||||
|
calculateResumeDiffUseCase: CalculateResumeDiffUseCase = CalculateResumeDiffUseCase(),
|
||||||
|
onBack: () -> Unit = { navController.popBackStack() }
|
||||||
|
) {
|
||||||
|
val gson = remember { Gson() }
|
||||||
|
val firstJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_FIRST)
|
||||||
|
val secondJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_SECOND)
|
||||||
|
val first = remember(firstJson) { firstJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
||||||
|
val second =
|
||||||
|
remember(secondJson) { secondJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
||||||
|
|
||||||
|
if (first == null || second == null) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(Paddings.large),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text("Не удалось загрузить данные для сравнения")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val diff = remember(first, second) { calculateResumeDiffUseCase(first, second) }
|
||||||
|
|
||||||
|
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 = MaterialTheme.colorScheme.onBackground,
|
||||||
|
contentDescription = "go back"
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Сравнение версий",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontSize = 22.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.medium)
|
||||||
|
) {
|
||||||
|
VersionSummaryCard(title = "Версия 1", resume = first)
|
||||||
|
VersionSummaryCard(title = "Версия 2", resume = second)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Изменено:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
if (diff.changedFields.isEmpty()) {
|
||||||
|
Text("Изменений не найдено", style = MaterialTheme.typography.labelLarge)
|
||||||
|
} else {
|
||||||
|
diff.changedFields.forEach { field ->
|
||||||
|
Card(shape = MaterialTheme.shapes.small) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = Paddings.medium, vertical = Paddings.small),
|
||||||
|
text = field,
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
diff.changes.forEach { change ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = Paddings.small),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(Paddings.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(change.title, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(change.body, style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VersionSummaryCard(title: String, resume: ResumeModel) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Paddings.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(title, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Text(resume.position, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(resume.prediction.toSalaryRangeString(), style = MaterialTheme.typography.labelLarge)
|
||||||
|
Text(resume.city, style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+7
-5
@@ -1,15 +1,17 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
package com.prodhack.moscow2025.presentation.screens.diffScreen
|
||||||
|
|
||||||
import com.prodhack.moscow2025.domain.usecase.resumes.LoadHistoryUseCase
|
import androidx.paging.PagingData
|
||||||
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeHistoryUseCase
|
||||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Provided
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class ResumeDiffViewModel(
|
class ResumeDiffViewModel(
|
||||||
@Provided resumeId: String,
|
@Provided resumeId: String,
|
||||||
private val loadHistoryUseCase: LoadHistoryUseCase
|
loadResumeHistoryUseCase: LoadResumeHistoryUseCase
|
||||||
): BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
val history: Flow<PagingData<ResumeModel>> = loadResumeHistoryUseCase(resumeId)
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -82,7 +82,7 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
|
|||||||
Box {
|
Box {
|
||||||
ResumeDetailsContent(
|
ResumeDetailsContent(
|
||||||
resume = resume,
|
resume = resume,
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.navigate(AppDestination.Main.route) },
|
||||||
onHistory = {
|
onHistory = {
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
AppDestination.ResumeHistory.route,
|
AppDestination.ResumeHistory.route,
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ class ResumeDetailsViewModel(
|
|||||||
resumeState.collect {
|
resumeState.collect {
|
||||||
val data = (it as? UIState.Success)?.data
|
val data = (it as? UIState.Success)?.data
|
||||||
if (data?.prediction == null) {
|
if (data?.prediction == null) {
|
||||||
// startPredictionPolling()
|
startPredictionPolling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+188
-96
@@ -1,7 +1,9 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -14,33 +16,32 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.paging.compose.LazyPagingItems
|
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
import com.prodhack.moscow2025.domain.usecase.resumes.CalculateResumeDiffUseCase
|
import com.prodhack.moscow2025.domain.usecase.resumes.CalculateResumeDiffUseCase
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
|
import com.prodhack.moscow2025.presentation.navigation.navigate
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
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.toSalaryRangeString
|
||||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -62,52 +63,111 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
val typography = MaterialTheme.typography
|
val typography = MaterialTheme.typography
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val expandedState = remember { mutableStateMapOf<Int, Boolean>() }
|
val expandedState = remember { mutableStateMapOf<Int, Boolean>() }
|
||||||
|
val selected = remember { mutableStateMapOf<String, ResumeModel>() }
|
||||||
|
val selectedOrder = remember { mutableListOf<String>() }
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = Paddings.large)
|
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Column(
|
||||||
Row(
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxWidth(),
|
.fillMaxSize()
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
.padding(horizontal = Paddings.large),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Icon(
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
modifier = Modifier
|
Row(
|
||||||
.size(24.dp)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.rotate(180f)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
.noRippleClickable(onBack),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
painter = painterResource(R.drawable.ic_arr_details),
|
) {
|
||||||
tint = colorScheme.onBackground,
|
Icon(
|
||||||
contentDescription = "go back"
|
modifier = Modifier
|
||||||
)
|
.size(24.dp)
|
||||||
Text(
|
.rotate(180f)
|
||||||
text = "История резюме",
|
.noRippleClickable(onBack),
|
||||||
style = typography.titleLarge,
|
painter = painterResource(R.drawable.ic_arr_details),
|
||||||
fontSize = 22.sp
|
tint = colorScheme.onBackground,
|
||||||
)
|
contentDescription = "go back"
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
)
|
||||||
}
|
Text(
|
||||||
|
text = "История резюме",
|
||||||
|
style = typography.titleLarge,
|
||||||
|
fontSize = 22.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
LazyColumn(
|
viewModel.resumePosition.FoldUIStateWithGlobalCallbacks {
|
||||||
verticalArrangement = Arrangement.spacedBy(Paddings.medium)
|
Text(
|
||||||
) {
|
text = it,
|
||||||
items(items.itemCount) { index ->
|
style = typography.titleLarge,
|
||||||
val version = items[index] ?: return@items
|
fontSize = 20.sp
|
||||||
val previous = if ((index + 1) < items.itemCount) items[index + 1] else null
|
|
||||||
val expanded = expandedState[index] ?: false
|
|
||||||
HistoryCard(
|
|
||||||
current = version,
|
|
||||||
previous = previous,
|
|
||||||
expanded = expanded,
|
|
||||||
onToggle = { expandedState[index] = !expanded },
|
|
||||||
calculateResumeDiffUseCase = calculateResumeDiffUseCase
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.medium)
|
||||||
|
) {
|
||||||
|
items(items.itemCount) { index ->
|
||||||
|
val version = items[index] ?: return@items
|
||||||
|
val previous = if ((index + 1) < items.itemCount) items[index + 1] else null
|
||||||
|
val expanded = expandedState[index] ?: false
|
||||||
|
val isSelected = selected.contains(version.id)
|
||||||
|
val canSelectMore = isSelected || selected.size < 2
|
||||||
|
|
||||||
|
HistoryCard(
|
||||||
|
current = version,
|
||||||
|
previous = previous,
|
||||||
|
expanded = expanded,
|
||||||
|
onToggle = { expandedState[index] = !expanded },
|
||||||
|
calculateResumeDiffUseCase = calculateResumeDiffUseCase,
|
||||||
|
isSelected = isSelected,
|
||||||
|
enabled = canSelectMore,
|
||||||
|
onSelectToggle = {
|
||||||
|
if (isSelected) {
|
||||||
|
selected.remove(version.id)
|
||||||
|
selectedOrder.remove(version.id)
|
||||||
|
} else if (selected.size < 2) {
|
||||||
|
selected[version.id] = version
|
||||||
|
selectedOrder.add(version.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { Spacer(modifier = Modifier.height(Paddings.large * 3)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected.size == 2) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = Paddings.large),
|
||||||
|
onClick = {
|
||||||
|
val first = selected[selectedOrder.getOrNull(0)]
|
||||||
|
val second = selected[selectedOrder.getOrNull(1)]
|
||||||
|
val gson = Gson()
|
||||||
|
navController.navigate(
|
||||||
|
AppDestination.ResumeDiff.route,
|
||||||
|
Bundle().apply {
|
||||||
|
putString(AppDestination.ResumeDiff.ARG_FIRST, gson.toJson(first))
|
||||||
|
putString(AppDestination.ResumeDiff.ARG_SECOND, gson.toJson(second))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_checkmark),
|
||||||
|
contentDescription = "compare"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = { Text(text = "Сравнить") },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +179,10 @@ private fun HistoryCard(
|
|||||||
previous: ResumeModel?,
|
previous: ResumeModel?,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
calculateResumeDiffUseCase: CalculateResumeDiffUseCase
|
calculateResumeDiffUseCase: CalculateResumeDiffUseCase,
|
||||||
|
isSelected: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
onSelectToggle: () -> Unit
|
||||||
) {
|
) {
|
||||||
val typography = MaterialTheme.typography
|
val typography = MaterialTheme.typography
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -128,79 +191,108 @@ private fun HistoryCard(
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
onClick = onToggle,
|
onClick = onToggle,
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
enabled = enabled || isSelected
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(Paddings.medium),
|
.padding(Paddings.medium),
|
||||||
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
horizontalArrangement = Arrangement.spacedBy(Paddings.medium),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Column(
|
||||||
Text(
|
modifier = Modifier.weight(1f),
|
||||||
salaryDiff,
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
style = typography.titleMedium,
|
|
||||||
color = colorScheme.primary,
|
|
||||||
fontSize = 20.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(Paddings.small))
|
|
||||||
Text(
|
|
||||||
current.prediction.toSalaryRangeString(),
|
|
||||||
style = typography.labelLarge,
|
|
||||||
fontSize = 18.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
"Изменено:",
|
|
||||||
style = typography.titleMedium,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(
|
|
||||||
Paddings.small
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
|
||||||
Paddings.small
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
changes.changedFields.forEach { skillName ->
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
TBubble(text = skillName)
|
if (previous != null) {
|
||||||
|
Text(
|
||||||
|
salaryDiff,
|
||||||
|
style = typography.titleMedium,
|
||||||
|
color = colorScheme.primary,
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(Paddings.small))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
current.prediction.toSalaryRangeString(),
|
||||||
|
style = typography.labelLarge,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
if (previous != null) {
|
||||||
Spacer(modifier = Modifier.height(Paddings.small))
|
Text(
|
||||||
if (expanded.not()) {
|
"Изменено:",
|
||||||
Text(
|
style = typography.titleMedium,
|
||||||
"Подробнее",
|
fontSize = 16.sp
|
||||||
style = typography.labelLarge,
|
)
|
||||||
textDecoration = TextDecoration.Underline,
|
FlowRow(
|
||||||
fontSize = 14.sp
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
}
|
Paddings.small
|
||||||
AnimatedVisibility(visible = expanded) {
|
),
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(Paddings.small)) {
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
changes.changes.forEach { change ->
|
Paddings.small
|
||||||
Column {
|
)
|
||||||
Text(change.title, style = typography.titleMedium)
|
) {
|
||||||
Text(change.body, style = typography.labelLarge)
|
changes.changedFields.forEach { skillName ->
|
||||||
|
TBubble(text = skillName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
if (expanded.not()) {
|
||||||
|
Text(
|
||||||
|
"Подробнее",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = expanded) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(Paddings.small)) {
|
||||||
|
changes.changes.forEach { change ->
|
||||||
|
Column {
|
||||||
|
Text(change.title, style = typography.titleMedium)
|
||||||
|
Text(change.body, style = typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"Первая версия",
|
||||||
|
style = typography.titleMedium,
|
||||||
|
color = colorScheme.primary,
|
||||||
|
fontSize = 20.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (enabled || isSelected) onSelectToggle()
|
||||||
|
},
|
||||||
|
enabled = enabled || isSelected
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateSalaryDiff(prev: Pair<Int?, Int?>?, current: Pair<Int?, Int?>?): String {
|
private fun calculateSalaryDiff(
|
||||||
|
prev: Pair<Float?, Float?>?,
|
||||||
|
current: Pair<Float?, Float?>?
|
||||||
|
): String {
|
||||||
val prevAvg = prev?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
val prevAvg = prev?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
||||||
val currAvg = current?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
val currAvg = current?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
||||||
return if (prevAvg != null && currAvg != null) {
|
return if (prevAvg != null && currAvg != null) {
|
||||||
val diff = currAvg - prevAvg
|
val diff = currAvg - prevAvg
|
||||||
val sign = if (diff >= 0) "+" else "-"
|
val sign = if (diff >= 0) "+" else "-"
|
||||||
"${sign}${kotlin.math.abs(diff).toInt()}₽"
|
"${sign}${(kotlin.math.abs(diff).toInt() / 1000) * 1000}₽"
|
||||||
} else {
|
} else {
|
||||||
"н/д"
|
"н/д"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Int>.averageOrNull(): Double? = if (isEmpty()) null else average()
|
private fun List<Float>.averageOrNull(): Double? = if (isEmpty()) null else average()
|
||||||
|
|||||||
+17
-2
@@ -1,17 +1,32 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.resumes.GetResumeInfoUseCase
|
||||||
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeHistoryUseCase
|
import com.prodhack.moscow2025.domain.usecase.resumes.LoadResumeHistoryUseCase
|
||||||
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
import com.prodhack.moscow2025.presentation.utils.base.BaseViewModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
import org.koin.core.annotation.Provided
|
import org.koin.core.annotation.Provided
|
||||||
|
|
||||||
@KoinViewModel
|
@KoinViewModel
|
||||||
class ResumeHistoryViewModel(
|
class ResumeHistoryViewModel(
|
||||||
@Provided resumeId: String,
|
@Provided private val resumeId: String,
|
||||||
loadResumeHistoryUseCase: LoadResumeHistoryUseCase
|
loadResumeHistoryUseCase: LoadResumeHistoryUseCase,
|
||||||
|
private val getResumeInfoUseCase: GetResumeInfoUseCase
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
val history: Flow<PagingData<ResumeModel>> = loadResumeHistoryUseCase(resumeId)
|
val history: Flow<PagingData<ResumeModel>> = loadResumeHistoryUseCase(resumeId)
|
||||||
|
|
||||||
|
val resumePosition = MutableUIStateFlow<String>()
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
getResumeInfoUseCase(resumeId = resumeId)
|
||||||
|
.map { it -> it.map { it.position } }.collectRequest(resumePosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.prodhack.moscow2025.presentation.utils
|
|||||||
|
|
||||||
import com.prodhack.moscow2025.domain.models.EducationGrades
|
import com.prodhack.moscow2025.domain.models.EducationGrades
|
||||||
import com.prodhack.moscow2025.domain.models.ExperienceType
|
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun ExperienceType.toReadableText(): String = when (this) {
|
fun ExperienceType.toReadableText(): String = when (this) {
|
||||||
ExperienceType.NoExperience -> "Нет опыта"
|
ExperienceType.NoExperience -> "Нет опыта"
|
||||||
@@ -22,10 +23,10 @@ fun EducationGrades.toReadableText(): String = when (this) {
|
|||||||
EducationGrades.Other -> "Другое"
|
EducationGrades.Other -> "Другое"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Pair<Int?, Int?>?.toSalaryRangeString(): String = when {
|
fun Pair<Float?, Float?>?.toSalaryRangeString(): String = when {
|
||||||
this == null -> "Загрузка..."
|
this == null -> "Загрузка..."
|
||||||
first != null && second != null -> "${first}₽ - ${second}₽"
|
first != null && second != null -> "${(first!!.roundToInt() / 1000) * 1000}₽ - ${(second!!.roundToInt() / 1000) * 1000}₽"
|
||||||
first != null -> "от ${first}₽"
|
first != null -> "от ${(first!!.roundToInt() / 1000) * 1000}₽"
|
||||||
second != null -> "до ${second}₽"
|
second != null -> "до ${(second!!.roundToInt() / 1000) * 1000}₽"
|
||||||
else -> "Ошибка"
|
else -> "Ошибка"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user