You've already forked RekomenciMobile
diff show
This commit is contained in:
+90
-2
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "b16cf19ddaafa74ea796a48650e53014",
|
||||
"identityHash": "153c4dcbf8d785dbfcd495eee39ae220",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "users",
|
||||
@@ -134,11 +134,99 @@
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "resumeId",
|
||||
"columnName": "resume_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "experienceType",
|
||||
"columnName": "experience_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "aboutMe",
|
||||
"columnName": "about_me",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keySkills",
|
||||
"columnName": "key_skills",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fromSalary",
|
||||
"columnName": "from_salary",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "toSalary",
|
||||
"columnName": "to_salary",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "recommendedSkills",
|
||||
"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": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"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, 'b16cf19ddaafa74ea796a48650e53014')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '153c4dcbf8d785dbfcd495eee39ae220')"
|
||||
]
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -4,14 +4,16 @@ 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.ResumeHistoryDao
|
||||
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.ResumeHistoryEntity
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeEntity
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.UserEntity
|
||||
|
||||
@Database(
|
||||
entities = [UserEntity::class, ResumeEntity::class],
|
||||
entities = [UserEntity::class, ResumeEntity::class, ResumeHistoryEntity::class],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
@@ -21,4 +23,5 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun cleanUpDao(): CleanUpDao
|
||||
abstract fun resumeDao(): ResumeDao
|
||||
abstract fun resumeHistoryDao(): ResumeHistoryDao
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.prodhack.moscow2025.data.data_providers.local_db.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.prodhack.moscow2025.data.base.BasePaginationDAO
|
||||
import com.prodhack.moscow2025.data.data_providers.local_db.entities.ResumeHistoryEntity
|
||||
|
||||
@Dao
|
||||
interface ResumeHistoryDao : BasePaginationDAO<ResumeHistoryEntity> {
|
||||
|
||||
@Query("DELETE FROM resume_history")
|
||||
override suspend fun clearAll()
|
||||
|
||||
@Upsert
|
||||
override suspend fun upsertAll(data: List<ResumeHistoryEntity>)
|
||||
|
||||
@Query("SELECT * FROM resume_history")
|
||||
override fun getPaginatedData(): PagingSource<Int, ResumeHistoryEntity>
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.prodhack.moscow2025.data.data_providers.local_db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.prodhack.moscow2025.domain.models.ExperienceType
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
|
||||
@Entity(tableName = "resume_history")
|
||||
data class ResumeHistoryEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
@ColumnInfo("resume_id")
|
||||
val resumeId: String,
|
||||
@ColumnInfo("experience_type")
|
||||
val experienceType: String,
|
||||
@ColumnInfo("about_me")
|
||||
val aboutMe: String,
|
||||
@ColumnInfo("key_skills")
|
||||
val keySkills: String,
|
||||
val position: String,
|
||||
@ColumnInfo("from_salary")
|
||||
val fromSalary: Int?,
|
||||
@ColumnInfo("to_salary")
|
||||
val toSalary: Int?,
|
||||
@ColumnInfo("recommended_skills")
|
||||
val recommendedSkills: String,
|
||||
val city: String,
|
||||
val experience: String,
|
||||
val education: String,
|
||||
val projects: String
|
||||
) {
|
||||
fun mapToDomain(): ResumeModel = ResumeModel(
|
||||
id = resumeId,
|
||||
position = position,
|
||||
about = aboutMe,
|
||||
experienceType = ExperienceType.valueOf(experienceType),
|
||||
skills = keySkills.split("|"),
|
||||
prediction = Pair(fromSalary, toSalary),
|
||||
recommendedSkills = recommendedSkills.split("|"),
|
||||
city = city,
|
||||
experience = JsonTypeConverters.toWorkExperienceList(experience),
|
||||
education = JsonTypeConverters.toEducationList(education),
|
||||
projects = JsonTypeConverters.toProjectList(projects)
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun fromDomain(model: ResumeModel): ResumeHistoryEntity = ResumeHistoryEntity(
|
||||
resumeId = model.id,
|
||||
experienceType = model.experienceType.name,
|
||||
aboutMe = model.about,
|
||||
keySkills = model.skills.joinToString("|"),
|
||||
position = model.position,
|
||||
fromSalary = model.prediction?.first,
|
||||
toSalary = model.prediction?.second,
|
||||
recommendedSkills = model.recommendedSkills?.joinToString("|") ?: "",
|
||||
city = model.city,
|
||||
experience = JsonTypeConverters.fromWorkExperienceList(model.experience),
|
||||
education = JsonTypeConverters.fromEducationList(model.education),
|
||||
projects = JsonTypeConverters.fromProjectList(model.projects)
|
||||
)
|
||||
}
|
||||
}
|
||||
+39
-3
@@ -4,6 +4,7 @@ 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.data_providers.local_db.entities.ResumeHistoryEntity
|
||||
import com.prodhack.moscow2025.data.dto.ResumeCreateDTO
|
||||
import com.prodhack.moscow2025.data.dto.ResumeDTO
|
||||
import com.prodhack.moscow2025.data.dto.ResumeIdDTO
|
||||
@@ -13,18 +14,17 @@ import com.prodhack.moscow2025.data.dto.mapToData
|
||||
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||
import com.prodhack.moscow2025.domain.models.ResumeCreationModel
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||
import com.prodhack.moscow2025.domain.utils.NetworkError
|
||||
import 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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@@ -37,6 +37,7 @@ class ResumeRepositoryImpl(
|
||||
override val defaultKtorClient = ktorClient.client
|
||||
|
||||
private val resumeDao = db.resumeDao()
|
||||
private val resumeHistoryDao = db.resumeHistoryDao()
|
||||
|
||||
override fun loadResumeList(): RemotePagingWrapper<ResumeModel> = paginatedRequest(
|
||||
pageSize = 20,
|
||||
@@ -110,4 +111,39 @@ class ResumeRepositoryImpl(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
override fun loadResumeHistory(resumeId: String): RemotePagingWrapper<ResumeModel> =
|
||||
paginatedRequest(
|
||||
pageSize = 10,
|
||||
dbDao = resumeHistoryDao,
|
||||
makeRequest = { offset, pageSize ->
|
||||
fetchResumeHistoryPage(resumeId, offset, pageSize).map { list ->
|
||||
list.map { ResumeHistoryEntity.fromDomain(it) }
|
||||
}
|
||||
}
|
||||
).map { pagingData -> pagingData.map { it.mapToDomain() } }
|
||||
|
||||
private suspend fun fetchResumeHistoryPage(
|
||||
resumeId: String,
|
||||
offset: Long,
|
||||
pageSize: Int
|
||||
): Result<List<ResumeModel>> {
|
||||
val mock = List(pageSize) { index ->
|
||||
val version = (offset + index + 1).toInt()
|
||||
ResumeModel(
|
||||
id = resumeId,
|
||||
position = "Android разработчик v$version",
|
||||
about = "Описание версии $version",
|
||||
skills = listOf("Kotlin", "Compose", "Room", "Ktor $version"),
|
||||
city = "Москва",
|
||||
experienceType = com.prodhack.moscow2025.domain.models.ExperienceType.Between3And6,
|
||||
experience = emptyList(),
|
||||
education = emptyList(),
|
||||
projects = emptyList(),
|
||||
prediction = Pair(200000 + version * 1000, 230000 + version * 1000),
|
||||
recommendedSkills = listOf("Coroutines", "Flows")
|
||||
)
|
||||
}
|
||||
return Result.success(mock)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ResumeRepository {
|
||||
fun loadResumeList(): RemotePagingWrapper<ResumeModel>
|
||||
fun loadResumeHistory(resumeId: String): RemotePagingWrapper<ResumeModel>
|
||||
|
||||
suspend fun suggestSkills(query: String): Result<List<String>>
|
||||
suspend fun createResume(resumeForm: ResumeCreationModel): Result<String>
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||
|
||||
data class ResumeDiff(
|
||||
val changedFields: List<String>,
|
||||
val changes: List<ChangeModel>
|
||||
)
|
||||
|
||||
data class ChangeModel(
|
||||
val title: String,
|
||||
val body: String
|
||||
)
|
||||
|
||||
class CalculateResumeDiffUseCase {
|
||||
operator fun invoke(previous: ResumeModel?, current: ResumeModel): ResumeDiff {
|
||||
if (previous == null) return ResumeDiff(
|
||||
changedFields = emptyList(),
|
||||
changes = emptyList()
|
||||
)
|
||||
|
||||
val changedFields = mutableListOf<String>()
|
||||
val changes = mutableListOf<ChangeModel>()
|
||||
|
||||
if (previous.position != current.position) {
|
||||
changedFields += "должность"
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
title = "Должность",
|
||||
body = "${previous.position} ->\n${current.position}"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (previous.about != current.about) {
|
||||
changedFields += "описание"
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
title = "Новое описание",
|
||||
body = current.about
|
||||
)
|
||||
)
|
||||
}
|
||||
if (previous.skills != current.skills) {
|
||||
changedFields += "навыки"
|
||||
val added = current.skills.toSet() - previous.skills.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
title = "Добавлены навыки",
|
||||
body = added.joinToString("\n")
|
||||
)
|
||||
)
|
||||
}
|
||||
val removed = previous.skills.toSet() - current.skills.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
title = "Удалены навыки",
|
||||
body = removed.joinToString("\n")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (previous.city != current.city) {
|
||||
changedFields += "город"
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Город",
|
||||
"${previous.city} ->\n${current.city}"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (previous.experienceType != current.experienceType) {
|
||||
changedFields += "опыт"
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Опыт",
|
||||
"${previous.experienceType.toReadableText()} ->\n${current.experienceType.toReadableText()}"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (previous.experience != current.experience) {
|
||||
changedFields += "опыт работы"
|
||||
|
||||
val added = current.experience.toSet() - previous.experience.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Добавлен опыт",
|
||||
added.joinToString("\n") { it.place }
|
||||
)
|
||||
)
|
||||
}
|
||||
val removed = previous.experience.toSet() - current.experience.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Удален опыт",
|
||||
removed.joinToString("\n") { it.place }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (previous.education != current.education) {
|
||||
changedFields += "образование"
|
||||
val added = current.education.toSet() - previous.education.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Добавлено образование",
|
||||
added.joinToString("\n") { it.place }
|
||||
)
|
||||
)
|
||||
}
|
||||
val removed = previous.education.toSet() - current.education.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Удалено образование",
|
||||
removed.joinToString("\n") { it.place }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (previous.projects != current.projects) {
|
||||
changedFields += "проекты"
|
||||
val added = current.projects.toSet() - previous.projects.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Добавлен проект",
|
||||
added.joinToString("\n") { it.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
val removed = previous.projects.toSet() - current.projects.toSet()
|
||||
|
||||
if (added.isNotEmpty()) {
|
||||
changes.add(
|
||||
ChangeModel(
|
||||
"Удален проект",
|
||||
removed.joinToString("\n") { it.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return ResumeDiff(
|
||||
changedFields = changedFields,
|
||||
changes = changes
|
||||
)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.prodhack.moscow2025.domain.usecase.resumes
|
||||
|
||||
import com.prodhack.moscow2025.domain.interfaces.resumes.ResumeRepository
|
||||
import com.prodhack.moscow2025.domain.models.ResumeModel
|
||||
import com.prodhack.moscow2025.domain.utils.RemotePagingWrapper
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
@Single
|
||||
class LoadResumeHistoryUseCase(
|
||||
private val resumeRepository: ResumeRepository
|
||||
) {
|
||||
operator fun invoke(resumeId: String): RemotePagingWrapper<ResumeModel> =
|
||||
resumeRepository.loadResumeHistory(resumeId)
|
||||
}
|
||||
@@ -23,6 +23,10 @@ sealed class AppDestination(val route: String) {
|
||||
|
||||
data object ResumeCreation: AppDestination("resume/creation")
|
||||
|
||||
data object ResumeHistory : AppDestination("resume/history") {
|
||||
const val ARG_ID = "id"
|
||||
}
|
||||
|
||||
data object ResumeEdit : AppDestination("resume/edit") {
|
||||
const val ARG_ID = "id"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.prodhack.moscow2025.presentation.screens.profile.ProfileScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.register.RegisterScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.resumeDetails.ResumeDetailsScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.resumeDetails.EditResumeScreen
|
||||
import com.prodhack.moscow2025.presentation.screens.resumeHistory.ResumeHistoryScreen
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCallbacks
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -132,10 +133,14 @@ fun TTasksNavHost(
|
||||
}, openResumeDetails = { id ->
|
||||
navController.navigate(AppDestination.ResumeDetails.route, Bundle().apply {
|
||||
putString(AppDestination.ResumeDetails.ARG_ID, id)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
composable(AppDestination.ResumeHistory.route) {
|
||||
ResumeHistoryScreen(navBackStackEntry = it) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -87,7 +87,14 @@ fun ErrorCollectorScope.ResumeDetailsScreen(
|
||||
ResumeDetailsContent(
|
||||
resume = resume,
|
||||
onBack = { navController.popBackStack() },
|
||||
onHistory = {}
|
||||
onHistory = {
|
||||
navController.navigate(
|
||||
AppDestination.ResumeHistory.route,
|
||||
Bundle().apply {
|
||||
putString(AppDestination.ResumeHistory.ARG_ID, resume.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
ExtendedFloatingActionButton(
|
||||
modifier = Modifier
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.lazy.LazyColumn
|
||||
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.mutableStateMapOf
|
||||
import androidx.compose.runtime.getValue
|
||||
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.rotate
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
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.components.standart.TBubble
|
||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
||||
import com.prodhack.moscow2025.presentation.utils.ErrorCollectorScope
|
||||
import com.prodhack.moscow2025.presentation.utils.toReadableText
|
||||
import com.prodhack.moscow2025.presentation.utils.toSalaryRangeString
|
||||
import com.prodhack.moscow2025.presentation.utils.ui.noRippleClickable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun ErrorCollectorScope.ResumeHistoryScreen(
|
||||
navBackStackEntry: NavBackStackEntry,
|
||||
calculateResumeDiffUseCase: CalculateResumeDiffUseCase = CalculateResumeDiffUseCase(),
|
||||
viewModel: ResumeHistoryViewModel = koinViewModel {
|
||||
parametersOf(
|
||||
navBackStackEntry.arguments?.getString(AppDestination.ResumeHistory.ARG_ID, "") ?: ""
|
||||
)
|
||||
},
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val items = viewModel.history.collectAsLazyPagingItems()
|
||||
|
||||
val typography = MaterialTheme.typography
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val expandedState = remember { mutableStateMapOf<Int, Boolean>() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = Paddings.large)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(Paddings.large))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.rotate(180f)
|
||||
.noRippleClickable(onBack),
|
||||
painter = painterResource(R.drawable.ic_arr_details),
|
||||
tint = colorScheme.onBackground,
|
||||
contentDescription = "go back"
|
||||
)
|
||||
Text(
|
||||
text = "История резюме",
|
||||
style = typography.titleLarge,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
|
||||
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
|
||||
HistoryCard(
|
||||
current = version,
|
||||
previous = previous,
|
||||
expanded = expanded,
|
||||
onToggle = { expandedState[index] = !expanded },
|
||||
calculateResumeDiffUseCase = calculateResumeDiffUseCase
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun HistoryCard(
|
||||
current: ResumeModel,
|
||||
previous: ResumeModel?,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
calculateResumeDiffUseCase: CalculateResumeDiffUseCase
|
||||
) {
|
||||
val typography = MaterialTheme.typography
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val changes = calculateResumeDiffUseCase(previous, current)
|
||||
val salaryDiff = calculateSalaryDiff(previous?.prediction, current.prediction)
|
||||
|
||||
Card(
|
||||
onClick = onToggle,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Paddings.medium),
|
||||
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
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
|
||||
)
|
||||
}
|
||||
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 ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateSalaryDiff(prev: Pair<Int?, Int?>?, current: Pair<Int?, Int?>?): String {
|
||||
val prevAvg = prev?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
||||
val currAvg = current?.let { listOfNotNull(it.first, it.second).averageOrNull() }
|
||||
return if (prevAvg != null && currAvg != null) {
|
||||
val diff = currAvg - prevAvg
|
||||
val sign = if (diff >= 0) "+" else "-"
|
||||
"${sign}${kotlin.math.abs(diff).toInt()}₽"
|
||||
} else {
|
||||
"н/д"
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Int>.averageOrNull(): Double? = if (isEmpty()) null else average()
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.prodhack.moscow2025.presentation.screens.resumeHistory
|
||||
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import org.koin.android.annotation.KoinViewModel
|
||||
import org.koin.core.annotation.Provided
|
||||
|
||||
@KoinViewModel
|
||||
class ResumeHistoryViewModel(
|
||||
@Provided resumeId: String,
|
||||
loadResumeHistoryUseCase: LoadResumeHistoryUseCase
|
||||
) : BaseViewModel() {
|
||||
val history: Flow<PagingData<ResumeModel>> = loadResumeHistoryUseCase(resumeId)
|
||||
}
|
||||
Reference in New Issue
Block a user