You've already forked RekomenciMobile
Merge branch 'master' of gitlab.prodcontest.com:team-39/mobile
* 'master' of gitlab.prodcontest.com:team-39/mobile: added readme test creds on signUp fcm token sending fixed order on diff screen versions diff
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
MoscowHackatonTemplate — экраны приложения
|
||||||
|
|
||||||
|
- Регистрация (RegisterScreen): ввод email и пароля, диалог генерации тестовых данных, переход в приложение после успешной регистрации. Статус: Готов.
|
||||||
|
- Вход (LoginScreen): авторизация по email/паролю, подсказка тестовых аккаунтов, переход к регистрации. Статус: Готов.
|
||||||
|
- Заполнение профиля (FillProfileScreen): сбор имени, фамилии, телефона с выбором страны, завершение онбординга. Статус: Готов.
|
||||||
|
- Главный экран (MainScreen): список резюме с пагинацией и pull-to-refresh, переход к созданию и деталям резюме, запрос разрешения на уведомления. Статус: Готов.
|
||||||
|
- Создание резюме (CreateResumeScreen): анкета по должности, городу, навыкам, опыту, образованию и проектам с отправкой на расчёт зарплаты/создание карточки. Статус: Готов.
|
||||||
|
- Редактирование резюме (EditResumeScreen): тот же интерфейс создания, но загружает существующие данные и пересчитывает прогноз. Статус: Готов.
|
||||||
|
- Детали резюме (ResumeDetailsScreen): отображение полной карточки резюме, переход к истории версий и к редактированию. Статус: Готов.
|
||||||
|
- История резюме (ResumeHistoryScreen): список версий, раскрытие изменений, выбор двух версий для сравнения, переход на экран diff. Статус: Готов.
|
||||||
|
- Сравнение версий (ResumeDiffScreen): визуальное сравнение выбранных версий резюме (зарплата, навыки, опыт, образование) с подсветкой изменений. Статус: Готов.
|
||||||
|
- Профиль (ProfileScreen): редактирование данных пользователя (имя, фамилия, телефон), сохранение и выход из аккаунта. Статус: Готов.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.prodhack.moscow2025.data.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FcmTokenDTO(
|
||||||
|
@SerialName("device_id")
|
||||||
|
val deviceId: String
|
||||||
|
)
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.prodhack.moscow2025.data.repImplementations
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.data.base.BaseRepository
|
||||||
|
import com.prodhack.moscow2025.data.data_providers.api.ApiKtorClient
|
||||||
|
import com.prodhack.moscow2025.data.dto.FcmTokenDTO
|
||||||
|
import com.prodhack.moscow2025.domain.interfaces.FCMRepository
|
||||||
|
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 org.koin.core.annotation.Single
|
||||||
|
|
||||||
|
@Single
|
||||||
|
class FCMRepositoryImpl(
|
||||||
|
val ktorClient: ApiKtorClient
|
||||||
|
) : FCMRepository, BaseRepository() {
|
||||||
|
|
||||||
|
override val defaultKtorClient = ktorClient.client
|
||||||
|
|
||||||
|
override suspend fun sendFCMToken(token: String) {
|
||||||
|
networkRequest<String> {
|
||||||
|
method = HttpMethod.Post
|
||||||
|
url("/notifications/register_device")
|
||||||
|
setBody(FcmTokenDTO(token))
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.prodhack.moscow2025.domain.interfaces
|
||||||
|
|
||||||
|
interface FCMRepository {
|
||||||
|
|
||||||
|
suspend fun sendFCMToken(token: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.prodhack.moscow2025.domain.usecase.auth
|
||||||
|
|
||||||
|
import com.prodhack.moscow2025.domain.interfaces.FCMRepository
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
|
||||||
|
@Single
|
||||||
|
class SendFCMTokenUseCase(private val fcmRepository: FCMRepository) {
|
||||||
|
suspend operator fun invoke(token: String) = fcmRepository.sendFCMToken(token = token)
|
||||||
|
}
|
||||||
@@ -14,12 +14,15 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.CheckSessionUseCase
|
import com.prodhack.moscow2025.domain.usecase.auth.CheckSessionUseCase
|
||||||
|
import com.prodhack.moscow2025.domain.usecase.auth.SendFCMTokenUseCase
|
||||||
import com.prodhack.moscow2025.domain.usecase.auth.SessionState
|
import com.prodhack.moscow2025.domain.usecase.auth.SessionState
|
||||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
import com.prodhack.moscow2025.presentation.navigation.TTasksApp
|
import com.prodhack.moscow2025.presentation.navigation.TTasksApp
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import kotlin.getValue
|
import kotlin.getValue
|
||||||
@@ -32,6 +35,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private val checkSessionUseCase: CheckSessionUseCase by inject()
|
private val checkSessionUseCase: CheckSessionUseCase by inject()
|
||||||
|
|
||||||
|
private val sendFCMTokenUseCase: SendFCMTokenUseCase by inject()
|
||||||
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
|
private val sessionDestinationState = MutableStateFlow<AppDestination?>(null)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -65,20 +69,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val sessionDestination by sessionDestinationState.collectAsState()
|
val sessionDestination by sessionDestinationState.collectAsState()
|
||||||
TTasksApp(sessionDestination = sessionDestination, context = this)
|
TTasksApp(
|
||||||
LaunchedEffect(Unit) {
|
sessionDestination = sessionDestination,
|
||||||
requestPermissions(
|
context = this,
|
||||||
arrayOf(Manifest.permission.ACCESS_NOTIFICATION_POLICY), 123
|
requestNotifyPermissions = {
|
||||||
)
|
|
||||||
FirebaseMessaging.getInstance().token
|
|
||||||
.addOnCompleteListener { task ->
|
|
||||||
if (task.isSuccessful) {
|
|
||||||
val token = task.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAndRequestNotificationPermission()
|
checkAndRequestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +86,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
this,
|
this,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) == PackageManager.PERMISSION_GRANTED -> {
|
) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
// Разрешение уже есть, получаем токен
|
|
||||||
getFCMToken()
|
getFCMToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// Запрашиваем разрешение
|
|
||||||
requestPermissions(
|
requestPermissions(
|
||||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
123
|
123
|
||||||
@@ -102,17 +97,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Для версий ниже Android 13 разрешение не требуется
|
|
||||||
getFCMToken()
|
getFCMToken()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFCMToken() {
|
fun getFCMToken() {
|
||||||
FirebaseMessaging.getInstance().token
|
FirebaseMessaging.getInstance().token
|
||||||
.addOnCompleteListener { task ->
|
.addOnCompleteListener { task ->
|
||||||
if (task.isSuccessful) {
|
if (task.isSuccessful) {
|
||||||
val token = task.result
|
val token = task.result
|
||||||
Log.d("TOKEN", token)
|
Log.d("TOKEN", token)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
sendFCMTokenUseCase(token)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("TOKEN", "Failed to get token", task.exception)
|
Log.e("TOKEN", "Failed to get token", task.exception)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.prodhack.moscow2025.presentation.utils.ui.SnackbarStyle
|
|||||||
fun TTasksApp(
|
fun TTasksApp(
|
||||||
appState: TTasksAppState = rememberTTasksAppState(),
|
appState: TTasksAppState = rememberTTasksAppState(),
|
||||||
context: Context,
|
context: Context,
|
||||||
|
requestNotifyPermissions: () -> Unit,
|
||||||
sessionDestination: AppDestination? = null
|
sessionDestination: AppDestination? = null
|
||||||
) {
|
) {
|
||||||
MoscowHackatonTemplateTheme {
|
MoscowHackatonTemplateTheme {
|
||||||
@@ -99,7 +100,8 @@ fun TTasksApp(
|
|||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier.padding(padding),
|
||||||
sessionDestination = sessionDestination,
|
sessionDestination = sessionDestination,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
context = context
|
context = context,
|
||||||
|
requestNotifyPermissions = requestNotifyPermissions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ fun TTasksNavHost(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sessionDestination: AppDestination? = null,
|
sessionDestination: AppDestination? = null,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
requestNotifyPermissions: () -> Unit,
|
||||||
snackbarHostState: SnackbarHostState
|
snackbarHostState: SnackbarHostState
|
||||||
) {
|
) {
|
||||||
val startDestination = sessionDestination?.route ?: AppDestination.Login.route
|
val startDestination = sessionDestination?.route ?: AppDestination.Login.route
|
||||||
@@ -100,7 +101,8 @@ fun TTasksNavHost(
|
|||||||
})
|
})
|
||||||
}, openCreateResume = {
|
}, openCreateResume = {
|
||||||
navController.navigate(AppDestination.ResumeCreation.route)
|
navController.navigate(AppDestination.ResumeCreation.route)
|
||||||
}
|
},
|
||||||
|
requestNotifyPermissions = requestNotifyPermissions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+481
-66
@@ -2,6 +2,7 @@ package com.prodhack.moscow2025.presentation.screens.diffScreen
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -10,7 +11,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardColors
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
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
|
||||||
@@ -21,29 +26,34 @@ 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.font.FontWeight
|
||||||
|
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 com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
|
import com.prodhack.moscow2025.domain.models.Education
|
||||||
|
import com.prodhack.moscow2025.domain.models.Project
|
||||||
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.models.WorkExperience
|
||||||
|
import com.prodhack.moscow2025.presentation.components.standart.TBubble
|
||||||
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
import com.prodhack.moscow2025.presentation.navigation.AppDestination
|
||||||
import com.prodhack.moscow2025.presentation.theme.Paddings
|
import com.prodhack.moscow2025.presentation.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
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorCollectorScope.ResumeDiffScreen(
|
fun ErrorCollectorScope.ResumeDiffScreen(
|
||||||
navBackStackEntry: NavBackStackEntry,
|
navBackStackEntry: NavBackStackEntry,
|
||||||
calculateResumeDiffUseCase: CalculateResumeDiffUseCase = CalculateResumeDiffUseCase(),
|
|
||||||
onBack: () -> Unit = { navController.popBackStack() }
|
onBack: () -> Unit = { navController.popBackStack() }
|
||||||
) {
|
) {
|
||||||
val gson = remember { Gson() }
|
val gson = remember { Gson() }
|
||||||
val firstJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_FIRST)
|
val firstJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_FIRST)
|
||||||
val secondJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_SECOND)
|
val secondJson = navBackStackEntry.arguments?.getString(AppDestination.ResumeDiff.ARG_SECOND)
|
||||||
val first = remember(firstJson) { firstJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
val first =
|
||||||
|
remember(firstJson) { firstJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
||||||
val second =
|
val second =
|
||||||
remember(secondJson) { secondJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
remember(secondJson) { secondJson?.let { gson.fromJson(it, ResumeModel::class.java) } }
|
||||||
|
|
||||||
@@ -60,7 +70,17 @@ fun ErrorCollectorScope.ResumeDiffScreen(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val diff = remember(first, second) { calculateResumeDiffUseCase(first, second) }
|
val scrollState = rememberScrollState()
|
||||||
|
val salaryDiff =
|
||||||
|
remember(first, second) { calculateSalaryDiff(first.prediction, second.prediction) }
|
||||||
|
val addedSkills = remember(first, second) { second.skills.toSet() - first.skills.toSet() }
|
||||||
|
val removedSkills = remember(first, second) { first.skills.toSet() - second.skills.toSet() }
|
||||||
|
val addedExperience = remember(first, second) { second.experience - first.experience }
|
||||||
|
val removedExperience = remember(first, second) { first.experience - second.experience }
|
||||||
|
val addedEducation = remember(first, second) { second.education - first.education }
|
||||||
|
val removedEducation = remember(first, second) { first.education - second.education }
|
||||||
|
val addedProjects = remember(first, second) { second.projects - first.projects }
|
||||||
|
val removedProjects = remember(first, second) { first.projects - second.projects }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -90,69 +110,20 @@ fun ErrorCollectorScope.ResumeDiffScreen(
|
|||||||
Spacer(modifier = Modifier.size(24.dp))
|
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(
|
Column(
|
||||||
modifier = Modifier.padding(Paddings.medium),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
Text(change.title, style = MaterialTheme.typography.titleMedium)
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
Text(change.body, style = MaterialTheme.typography.labelLarge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VersionSummaryCard(title: String, resume: ResumeModel) {
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
),
|
||||||
shape = MaterialTheme.shapes.medium
|
shape = MaterialTheme.shapes.medium
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -161,10 +132,454 @@ private fun VersionSummaryCard(title: String, resume: ResumeModel) {
|
|||||||
.padding(Paddings.medium),
|
.padding(Paddings.medium),
|
||||||
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
) {
|
) {
|
||||||
Text(title, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
Text(
|
||||||
Text(resume.position, style = MaterialTheme.typography.titleMedium)
|
text = "Разница в зарплате",
|
||||||
Text(resume.prediction.toSalaryRangeString(), style = MaterialTheme.typography.labelLarge)
|
style = MaterialTheme.typography.labelLarge,
|
||||||
Text(resume.city, style = MaterialTheme.typography.labelMedium)
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = salaryDiff,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 22.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
|
|
||||||
|
SectionContainer {
|
||||||
|
DiffValueRow(
|
||||||
|
title = "Должность",
|
||||||
|
previous = first.position,
|
||||||
|
current = second.position
|
||||||
|
)
|
||||||
|
DiffValueRow(
|
||||||
|
title = "Город",
|
||||||
|
previous = first.city,
|
||||||
|
current = second.city
|
||||||
|
)
|
||||||
|
DiffValueRow(
|
||||||
|
title = "Опыт",
|
||||||
|
previous = first.experienceType.toReadableText(),
|
||||||
|
current = second.experienceType.toReadableText()
|
||||||
|
)
|
||||||
|
DiffValueRow(
|
||||||
|
title = "Прогноз зарплаты",
|
||||||
|
previous = first.prediction.toSalaryRangeString(),
|
||||||
|
current = second.prediction.toSalaryRangeString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "О себе") {
|
||||||
|
DiffTextBlock(
|
||||||
|
previous = first.about.ifBlank { "Описание отсутствует" },
|
||||||
|
current = second.about.ifBlank { "Описание отсутствует" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Ключевые навыки") {
|
||||||
|
SkillsDiffBlock(addedSkills = addedSkills, removedSkills = removedSkills)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Опыт работы") {
|
||||||
|
WorkExperienceDiffBlock(
|
||||||
|
added = addedExperience,
|
||||||
|
removed = removedExperience
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Образование") {
|
||||||
|
EducationDiffBlock(
|
||||||
|
added = addedEducation,
|
||||||
|
removed = removedEducation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.medium))
|
||||||
|
|
||||||
|
SectionContainer(title = "Проекты") {
|
||||||
|
ProjectDiffBlock(
|
||||||
|
added = addedProjects,
|
||||||
|
removed = removedProjects
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.large * 3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String = "",
|
||||||
|
colors: CardColors = CardDefaults.cardColors(),
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = colors,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Paddings.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
if (title.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DiffValueRow(
|
||||||
|
title: String,
|
||||||
|
previous: String,
|
||||||
|
current: String
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val changed = previous != current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = colorScheme.primary
|
||||||
|
)
|
||||||
|
if (changed) {
|
||||||
|
Text(
|
||||||
|
text = previous,
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = TextDecoration.LineThrough),
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = current,
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
fontWeight = if (changed) FontWeight.Bold else FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DiffTextBlock(
|
||||||
|
previous: String,
|
||||||
|
current: String
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val changed = previous != current
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
Text(
|
||||||
|
text = previous,
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = TextDecoration.LineThrough),
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = current,
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text("Без изменений", style = typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SkillsDiffBlock(
|
||||||
|
addedSkills: Set<String>,
|
||||||
|
removedSkills: Set<String>
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
|
||||||
|
if (addedSkills.isEmpty() && removedSkills.isEmpty()) {
|
||||||
|
Text("Без изменений", style = typography.bodyMedium)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedSkills.isNotEmpty()) {
|
||||||
|
Text("Добавлено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
addedSkills.forEach { skill ->
|
||||||
|
TBubble(text = skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedSkills.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
Text("Удалено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Paddings.small),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
removedSkills.forEach { skill ->
|
||||||
|
TBubble(text = skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WorkExperienceDiffBlock(
|
||||||
|
added: List<WorkExperience>,
|
||||||
|
removed: List<WorkExperience>
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
|
||||||
|
if (added.isEmpty() && removed.isEmpty()) {
|
||||||
|
Text("Изменений нет", style = typography.bodyMedium)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added.isNotEmpty()) {
|
||||||
|
Text("Добавлено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
added.forEachIndexed { index, work ->
|
||||||
|
WorkExperienceCard(index = index, workExperience = work)
|
||||||
|
if (index != added.lastIndex || removed.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed.isNotEmpty()) {
|
||||||
|
Text("Удалено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
removed.forEachIndexed { index, work ->
|
||||||
|
WorkExperienceCard(index = index, workExperience = work, isRemoved = true)
|
||||||
|
if (index != removed.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EducationDiffBlock(
|
||||||
|
added: List<Education>,
|
||||||
|
removed: List<Education>
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
|
||||||
|
if (added.isEmpty() && removed.isEmpty()) {
|
||||||
|
Text("Изменений нет", style = typography.bodyMedium)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added.isNotEmpty()) {
|
||||||
|
Text("Добавлено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
added.forEachIndexed { index, education ->
|
||||||
|
EducationCard(index = index, education = education)
|
||||||
|
if (index != added.lastIndex || removed.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed.isNotEmpty()) {
|
||||||
|
Text("Удалено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
removed.forEachIndexed { index, education ->
|
||||||
|
EducationCard(index = index, education = education, isRemoved = true)
|
||||||
|
if (index != removed.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProjectDiffBlock(
|
||||||
|
added: List<Project>,
|
||||||
|
removed: List<Project>
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
|
||||||
|
if (added.isEmpty() && removed.isEmpty()) {
|
||||||
|
Text("Изменений нет", style = typography.bodyMedium)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added.isNotEmpty()) {
|
||||||
|
Text("Добавлено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
added.forEachIndexed { index, project ->
|
||||||
|
ProjectCard(index = index, project = project)
|
||||||
|
if (index != added.lastIndex || removed.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed.isNotEmpty()) {
|
||||||
|
Text("Удалено", style = typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
removed.forEachIndexed { index, project ->
|
||||||
|
ProjectCard(index = index, project = project, isRemoved = true)
|
||||||
|
if (index != removed.lastIndex) {
|
||||||
|
Spacer(modifier = Modifier.height(Paddings.small))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WorkExperienceCard(
|
||||||
|
index: Int,
|
||||||
|
workExperience: WorkExperience,
|
||||||
|
isRemoved: Boolean = false
|
||||||
|
) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val textDecoration = if (isRemoved) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
val valueColor = if (isRemoved) colorScheme.onSurfaceVariant else colorScheme.onSurface
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Место №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = if (isRemoved) colorScheme.error else colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
workExperience.place,
|
||||||
|
style = typography.titleMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = workExperience.description,
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Длительность: ${workExperience.monthDuration.toMonthText()}",
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EducationCard(index: Int, education: Education, isRemoved: Boolean = false) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val textDecoration = if (isRemoved) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
val valueColor = if (isRemoved) colorScheme.onSurfaceVariant else colorScheme.onSurface
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Учебное место №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = if (isRemoved) colorScheme.error else colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
education.place,
|
||||||
|
style = typography.titleMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Ступень: ${education.grade.toReadableText()}",
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Специализация: ${education.specialization}",
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = education.description,
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProjectCard(index: Int, project: Project, isRemoved: Boolean = false) {
|
||||||
|
val typography = MaterialTheme.typography
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val textDecoration = if (isRemoved) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
val valueColor = if (isRemoved) colorScheme.onSurfaceVariant else colorScheme.onSurface
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Paddings.small)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Проект №${index + 1}",
|
||||||
|
style = typography.labelLarge,
|
||||||
|
color = if (isRemoved) colorScheme.error else colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
project.name,
|
||||||
|
style = typography.titleMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
project.description,
|
||||||
|
style = typography.bodyMedium.copy(textDecoration = textDecoration),
|
||||||
|
color = valueColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int?.toMonthText(): String = when {
|
||||||
|
this == null -> "Не указано"
|
||||||
|
this < 12 -> "$this мес."
|
||||||
|
else -> {
|
||||||
|
val years = this / 12
|
||||||
|
val months = this % 12
|
||||||
|
if (months == 0) "$years г." else "$years г. $months мес."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateSalaryDiff(
|
||||||
|
prev: Pair<Float?, Float?>?,
|
||||||
|
current: Pair<Float?, Float?>?
|
||||||
|
): 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() / 1000) * 1000}₽"
|
||||||
|
} else {
|
||||||
|
"н/д"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Float>.averageOrNull(): Double? = if (isEmpty()) null else average()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.main
|
package com.prodhack.moscow2025.presentation.screens.main
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -21,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -30,6 +32,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import com.prodhack.moscow2025.R
|
import com.prodhack.moscow2025.R
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
import com.prodhack.moscow2025.presentation.components.standart.BigButton
|
||||||
import com.prodhack.moscow2025.presentation.components.standart.TopLogo
|
import com.prodhack.moscow2025.presentation.components.standart.TopLogo
|
||||||
@@ -45,8 +48,13 @@ fun ErrorCollectorScope.MainScreen(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
openResumeDetails: (String) -> Unit,
|
openResumeDetails: (String) -> Unit,
|
||||||
openCreateResume: () -> Unit,
|
openCreateResume: () -> Unit,
|
||||||
|
requestNotifyPermissions: () -> Unit,
|
||||||
viewModel: MainScreenViewModel = koinViewModel()
|
viewModel: MainScreenViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
requestNotifyPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val items = viewModel.resumeList.collectAsLazyPagingItems()
|
val items = viewModel.resumeList.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
|||||||
+27
@@ -1,6 +1,7 @@
|
|||||||
package com.prodhack.moscow2025.presentation.screens.register
|
package com.prodhack.moscow2025.presentation.screens.register
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.width
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
@@ -58,6 +60,7 @@ fun ErrorCollectorScope.RegisterScreen(
|
|||||||
|
|
||||||
val formState by viewModel.formStateSignUp.collectAsState()
|
val formState by viewModel.formStateSignUp.collectAsState()
|
||||||
var errorText by remember { mutableStateOf("") }
|
var errorText by remember { mutableStateOf("") }
|
||||||
|
var isGeneratorDialogVisible by remember { mutableStateOf(false) }
|
||||||
val registerState by viewModel.registerState.collectAsStateWithCallbacks(
|
val registerState by viewModel.registerState.collectAsStateWithCallbacks(
|
||||||
onInputError = {
|
onInputError = {
|
||||||
errorText = it.error
|
errorText = it.error
|
||||||
@@ -76,6 +79,29 @@ fun ErrorCollectorScope.RegisterScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isGeneratorDialogVisible) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { isGeneratorDialogVisible = false },
|
||||||
|
title = { Text("Генерация данных") },
|
||||||
|
text = { Text("Случайный email и пароль будут подставлены в поля.") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.fillRandomCredentials()
|
||||||
|
isGeneratorDialogVisible = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Сгенерировать данные")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { isGeneratorDialogVisible = false }) {
|
||||||
|
Text("Отмена")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(registerState) {
|
LaunchedEffect(registerState) {
|
||||||
if (registerState is UIState.Success) {
|
if (registerState is UIState.Success) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
@@ -117,6 +143,7 @@ fun ErrorCollectorScope.RegisterScreen(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(200.dp)
|
.size(200.dp)
|
||||||
|
.clickable { isGeneratorDialogVisible = true }
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Регистрация",
|
text = "Регистрация",
|
||||||
|
|||||||
+26
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.annotation.KoinViewModel
|
import org.koin.android.annotation.KoinViewModel
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
data class RegisterFormState(
|
data class RegisterFormState(
|
||||||
val email: String = "",
|
val email: String = "",
|
||||||
@@ -56,6 +57,31 @@ class RegisterViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fillRandomCredentials() {
|
||||||
|
val password = randomPassword()
|
||||||
|
val email = randomEmail()
|
||||||
|
_formStateSignUp.update {
|
||||||
|
it.copy(
|
||||||
|
email = email,
|
||||||
|
password = password,
|
||||||
|
confirmPassword = password,
|
||||||
|
errors = emptyMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomEmail(): String {
|
||||||
|
val symbols = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
val name = (1..8).joinToString("") { symbols.random().toString() }
|
||||||
|
val domain = (1..5).joinToString("") { symbols.random().toString() }
|
||||||
|
return "$name@$domain.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomPassword(length: Int = 12): String {
|
||||||
|
val symbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
return (1..length).joinToString("") { symbols.random().toString() } + "!"
|
||||||
|
}
|
||||||
|
|
||||||
fun submit() {
|
fun submit() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val validation = validateFieldsUseCase.validateSignUp(
|
val validation = validateFieldsUseCase.validateSignUp(
|
||||||
|
|||||||
+2
-5
@@ -168,15 +168,12 @@ private fun ResumeDetailsContent(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(Paddings.large))
|
Spacer(modifier = Modifier.height(Paddings.large))
|
||||||
SectionContainer {
|
SectionContainer {
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(Paddings.small)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
resume.position,
|
resume.position,
|
||||||
style = typography.titleLarge,
|
style = typography.titleLarge,
|
||||||
fontSize = 28.sp
|
fontSize = 28.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (resume.prediction == null) {
|
if (resume.prediction == null) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
CircularProgressIndicator(modifier = Modifier.size(18.dp))
|
||||||
} else {
|
} else {
|
||||||
@@ -186,7 +183,7 @@ private fun ResumeDetailsContent(
|
|||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = resume.city,
|
text = resume.city,
|
||||||
style = typography.labelLarge,
|
style = typography.labelLarge,
|
||||||
|
|||||||
+16
-7
@@ -64,7 +64,7 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
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 selected = remember { mutableStateMapOf<String, ResumeModel>() }
|
||||||
val selectedOrder = remember { mutableListOf<String>() }
|
val selectedIndices = remember { mutableStateMapOf<String, Int>() }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -131,10 +131,10 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
onSelectToggle = {
|
onSelectToggle = {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selected.remove(version.id)
|
selected.remove(version.id)
|
||||||
selectedOrder.remove(version.id)
|
selectedIndices.remove(version.id)
|
||||||
} else if (selected.size < 2) {
|
} else if (selected.size < 2) {
|
||||||
selected[version.id] = version
|
selected[version.id] = version
|
||||||
selectedOrder.add(version.id)
|
selectedIndices[version.id] = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -143,14 +143,17 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.size == 2) {
|
if (selected.size > 0) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(bottom = Paddings.large),
|
.padding(bottom = Paddings.large),
|
||||||
onClick = {
|
onClick = {
|
||||||
val first = selected[selectedOrder.getOrNull(0)]
|
if (selected.size == 2) {
|
||||||
val second = selected[selectedOrder.getOrNull(1)]
|
val ordered = selected.toList()
|
||||||
|
.sortedByDescending { (id, _) -> selectedIndices[id] ?: Int.MIN_VALUE }
|
||||||
|
val first = ordered.getOrNull(0)?.second
|
||||||
|
val second = ordered.getOrNull(1)?.second
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
AppDestination.ResumeDiff.route,
|
AppDestination.ResumeDiff.route,
|
||||||
@@ -159,6 +162,7 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
putString(AppDestination.ResumeDiff.ARG_SECOND, gson.toJson(second))
|
putString(AppDestination.ResumeDiff.ARG_SECOND, gson.toJson(second))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -166,7 +170,12 @@ fun ErrorCollectorScope.ResumeHistoryScreen(
|
|||||||
contentDescription = "compare"
|
contentDescription = "compare"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = { Text(text = "Сравнить") },
|
text = {
|
||||||
|
Text(
|
||||||
|
text = if (selected.size == 2) "Сравнить" else "Выберите ещё 1",
|
||||||
|
style = typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user