emergency #1

Merged
trevor merged 2 commits from emergency into main 2025-10-16 07:32:01 +00:00
26 changed files with 812 additions and 285 deletions
Showing only changes of commit 3fea080626 - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -7,12 +7,27 @@ import com.google.gson.annotations.SerializedName
*/
data class UserProfile(
val id: Long = 0,
val username: String,
val email: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
val phone: String,
val username: String = "",
val email: String = "",
@SerializedName("first_name") val firstName: String = "",
@SerializedName("last_name") val lastName: String = "",
val phone: String = "",
@SerializedName("user_id") val userId: String = "",
@SerializedName("created_at") val createdAt: String = "",
@SerializedName("is_verified") val isVerified: Boolean = false
@SerializedName("is_verified") val isVerified: Boolean = false,
// Дополнительные поля для экрана редактирования профиля
val bio: String? = null,
@SerializedName("date_of_birth") val date_of_birth: String? = null,
// Экстренные контакты
@SerializedName("emergency_contact_1_name") val emergency_contact_1_name: String? = null,
@SerializedName("emergency_contact_1_phone") val emergency_contact_1_phone: String? = null,
@SerializedName("emergency_contact_2_name") val emergency_contact_2_name: String? = null,
@SerializedName("emergency_contact_2_phone") val emergency_contact_2_phone: String? = null,
// Настройки уведомлений и доступа
@SerializedName("emergency_notifications_enabled") val emergency_notifications_enabled: Boolean? = false,
@SerializedName("location_sharing_enabled") val location_sharing_enabled: Boolean? = false,
@SerializedName("push_notifications_enabled") val push_notifications_enabled: Boolean? = false
)

View File

@@ -2,91 +2,89 @@ package kr.smartsoltech.wellshe.ui.auth.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.runtime.livedata.observeAsState
import kr.smartsoltech.wellshe.ui.auth.AuthViewModel
import kr.smartsoltech.wellshe.util.isValidEmail
import kr.smartsoltech.wellshe.util.isValidPassword
import kr.smartsoltech.wellshe.util.isValidPhone
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RegisterScreen(
onNavigateBack: () -> Unit,
onRegisterSuccess: () -> Unit,
onRegisterSuccess: (email: String, password: String) -> Unit,
viewModel: AuthViewModel = hiltViewModel()
) {
val authState by viewModel.authState.observeAsState()
val isLoading by viewModel.isLoading.observeAsState()
val keyboardController = LocalSoftwareKeyboardController.current
val scrollState = rememberScrollState()
var email by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
var fullName by remember { mutableStateOf("") }
var phone by remember { mutableStateOf("") }
var emailError by remember { mutableStateOf<String?>(null) }
var usernameError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var firstNameError by remember { mutableStateOf<String?>(null) }
var lastNameError by remember { mutableStateOf<String?>(null) }
var phoneError by remember { mutableStateOf<String?>(null) }
var passwordVisible by remember { mutableStateOf(false) }
var confirmPasswordVisible by remember { mutableStateOf(false) }
var isFormValid by remember { mutableStateOf(false) }
var passwordsMatch by remember { mutableStateOf(true) }
fun validateForm() {
// Проверка email
emailError = if (!email.isValidEmail()) "Введите корректный email" else null
// FocusRequesters для полей формы
val usernameFocusRequester = remember { FocusRequester() }
val passwordFocusRequester = remember { FocusRequester() }
val confirmPasswordFocusRequester = remember { FocusRequester() }
val fullNameFocusRequester = remember { FocusRequester() }
val phoneFocusRequester = remember { FocusRequester() }
// Проверка имени пользователя
usernameError = if (username.length < 3) "Имя пользователя должно быть не менее 3 символов" else null
// Проверка пароля
passwordError = if (!password.isValidPassword())
"Пароль должен содержать не менее 8 символов, включая цифру, заглавную букву и специальный символ"
else null
// Проверка совпадения паролей
confirmPasswordError = if (password != confirmPassword) "Пароли не совпадают" else null
// Проверка имени и фамилии
firstNameError = if (firstName.isEmpty()) "Введите имя" else null
lastNameError = if (lastName.isEmpty()) "Введите фамилию" else null
// Проверка телефона
phoneError = if (!phone.isValidPhone()) "Введите корректный номер телефона" else null
// Проверка заполнения всех полей
isFormValid = email.isNotEmpty() && username.isNotEmpty() &&
password.isNotEmpty() && confirmPassword.isNotEmpty() &&
firstName.isNotEmpty() && lastName.isNotEmpty() && phone.isNotEmpty() &&
emailError == null && usernameError == null && passwordError == null &&
confirmPasswordError == null && firstNameError == null &&
lastNameError == null && phoneError == null
// Валидация формы
LaunchedEffect(email, username, password, confirmPassword, fullName, phone) {
passwordsMatch = password == confirmPassword
isFormValid = email.isNotBlank() && username.isNotBlank() &&
password.isNotBlank() && confirmPassword.isNotBlank() &&
fullName.isNotBlank() && phone.isNotBlank() &&
passwordsMatch &&
password.length >= 8 // Минимальная длина пароля
}
// Проверяем валидность формы при изменении любого поля
LaunchedEffect(email, username, password, confirmPassword, firstName, lastName, phone) {
validateForm()
}
// Обработка успешной регистрации
// Обработка состояния авторизации
LaunchedEffect(authState) {
if (authState is AuthViewModel.AuthState.RegistrationSuccess) {
onRegisterSuccess()
// При успешной регистрации сразу выполняем вход с теми же данными
onRegisterSuccess(email, password)
}
}
// Функция для выполнения регистрации
val performRegister = {
if (isFormValid && isLoading != true) {
keyboardController?.hide()
// Разделяем полное имя на имя и фамилию для сервера
val nameParts = fullName.trim().split(" ", limit = 2)
val firstName = nameParts[0]
val lastName = if (nameParts.size > 1) nameParts[1] else ""
viewModel.register(email, username, password, firstName, lastName, phone)
}
}
@@ -98,96 +96,164 @@ fun RegisterScreen(
.padding(16.dp)
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Регистрация",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
style = MaterialTheme.typography.headlineLarge
)
// Поле Email
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
isError = emailError != null,
supportingText = { emailError?.let { Text(it) } },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { usernameFocusRequester.requestFocus() }
)
)
// Поле Username
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Имя пользователя") },
isError = usernameError != null,
supportingText = { usernameError?.let { Text(it) } },
singleLine = true,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.focusRequester(usernameFocusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { passwordFocusRequester.requestFocus() }
)
)
// Поле Password
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Пароль") },
isError = passwordError != null,
supportingText = { passwordError?.let { Text(it) } },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(passwordFocusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { confirmPasswordFocusRequester.requestFocus() }
),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
contentDescription = if (passwordVisible) "Скрыть пароль" else "Показать пароль"
)
}
},
isError = password.isNotEmpty() && password.length < 8
)
if (password.isNotEmpty() && password.length < 8) {
Text(
text = "Пароль должен содержать минимум 8 символов",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.align(Alignment.Start)
)
}
// Поле для подтверждения пароля
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
label = { Text("Подтвердите пароль") },
isError = confirmPasswordError != null,
supportingText = { confirmPasswordError?.let { Text(it) } },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
modifier = Modifier
.fillMaxWidth()
.focusRequester(confirmPasswordFocusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { fullNameFocusRequester.requestFocus() }
),
trailingIcon = {
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
Icon(
imageVector = if (confirmPasswordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
contentDescription = if (confirmPasswordVisible) "Скрыть пароль" else "Показать пароль"
)
}
},
isError = confirmPassword.isNotEmpty() && !passwordsMatch
)
if (confirmPassword.isNotEmpty() && !passwordsMatch) {
Text(
text = "Пароли не совпадают",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.align(Alignment.Start)
)
}
// Поле для полного имени
OutlinedTextField(
value = firstName,
onValueChange = { firstName = it },
label = { Text("Имя") },
isError = firstNameError != null,
supportingText = { firstNameError?.let { Text(it) } },
value = fullName,
onValueChange = { fullName = it },
label = { Text("Полное имя") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = lastName,
onValueChange = { lastName = it },
label = { Text("Фамилия") },
isError = lastNameError != null,
supportingText = { lastNameError?.let { Text(it) } },
singleLine = true,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.focusRequester(fullNameFocusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { phoneFocusRequester.requestFocus() }
)
)
// Поле для телефона
OutlinedTextField(
value = phone,
onValueChange = { phone = it },
label = { Text("Телефон") },
isError = phoneError != null,
supportingText = { phoneError?.let { Text(it) } },
label = { Text("Номер телефона") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
modifier = Modifier
.fillMaxWidth()
.focusRequester(phoneFocusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Phone,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
performRegister()
}
)
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
// Кнопка регистрации
Button(
onClick = {
viewModel.register(email, username, password, firstName, lastName, phone)
},
onClick = { performRegister() },
enabled = isFormValid && isLoading != true,
modifier = Modifier
.fillMaxWidth()
@@ -203,18 +269,16 @@ fun RegisterScreen(
}
}
TextButton(
onClick = onNavigateBack,
modifier = Modifier.fillMaxWidth()
) {
Text("Вернуться к входу")
// Кнопка возврата к экрану входа
TextButton(onClick = onNavigateBack) {
Text("Уже есть аккаунт? Войти")
}
// Отображение ошибки
if (authState is AuthViewModel.AuthState.RegistrationError) {
Text(
text = (authState as AuthViewModel.AuthState.RegistrationError).message,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 8.dp)
color = MaterialTheme.colorScheme.error
)
}
}

View File

@@ -0,0 +1,29 @@
package kr.smartsoltech.wellshe.ui.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@Composable
fun PermissionRequestDialog(
permissionText: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Требуется разрешение") },
text = { Text(permissionText) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Настройки")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Отмена")
}
}
)
}

View File

@@ -46,8 +46,13 @@ fun AppNavGraph(
onNavigateBack = {
navController.popBackStack()
},
onRegisterSuccess = {
navController.popBackStack()
onRegisterSuccess = { email, password ->
// После успешной регистрации автоматически входим в систему с новыми учетными данными
authViewModel.login(email, password, true)
// Переходим на главный экран
navController.navigate(BottomNavItem.Cycle.route) {
popUpTo("login") { inclusive = true }
}
}
)
}

View File

@@ -0,0 +1,427 @@
package kr.smartsoltech.wellshe.ui.profile
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.ContactsContract
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import kr.smartsoltech.wellshe.model.auth.UserProfile
import kr.smartsoltech.wellshe.ui.components.PermissionRequestDialog
import kr.smartsoltech.wellshe.ui.profile.viewmodel.ProfileViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileEditScreen(
onNavigateBack: () -> Unit,
viewModel: ProfileViewModel = hiltViewModel()
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
// Состояние для профиля пользователя
var userProfile by remember { mutableStateOf(viewModel.userProfile.value ?: UserProfile()) }
// Состояния для показа диалогов выбора даты и разрешений
var showDatePicker by remember { mutableStateOf(false) }
var showContactPermissionDialog by remember { mutableStateOf(false) }
// Состояние для обработки запроса разрешений на контакты
var contactType by remember { mutableStateOf(ContactType.NONE) }
// Launcher для выбора контакта
val contactPickerLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { contactUri ->
// Обрабатываем выбранный контакт
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
)
context.contentResolver.query(contactUri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
val name = cursor.getString(nameIndex)
val phone = cursor.getString(numberIndex)
// Обновляем профиль в зависимости от типа контакта
when (contactType) {
ContactType.EMERGENCY_1 -> {
userProfile = userProfile.copy(
emergency_contact_1_name = name,
emergency_contact_1_phone = phone
)
}
ContactType.EMERGENCY_2 -> {
userProfile = userProfile.copy(
emergency_contact_2_name = name,
emergency_contact_2_phone = phone
)
}
else -> {}
}
}
}
}
}
}
// Запрос разрешения на доступ к контактам
val contactPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// Если разрешение получено, запускаем выбор контакта
val intent = Intent(Intent.ACTION_PICK).apply {
setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE)
}
contactPickerLauncher.launch(intent)
} else {
// Если разрешение не получено, показываем диалог
showContactPermissionDialog = true
}
}
// Проверка и запрос разрешения на доступ к контактам
fun checkAndRequestContactPermission(type: ContactType) {
contactType = type
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) -> {
// Разрешение уже есть, запускаем выбор контакта
val intent = Intent(Intent.ACTION_PICK).apply {
setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE)
}
contactPickerLauncher.launch(intent)
}
else -> {
// Запрашиваем разрешение
contactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS)
}
}
}
// Диалог выбора даты рождения
if (showDatePicker) {
val currentDate = userProfile.date_of_birth?.let {
try {
LocalDate.parse(it)
} catch (e: Exception) {
LocalDate.now().minusYears(20)
}
} ?: LocalDate.now().minusYears(20)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
val selectedDate = LocalDate.of(
viewModel.datePickerState.selectedDateMillis?.let { millis ->
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().year
} ?: currentDate.year,
viewModel.datePickerState.selectedDateMillis?.let { millis ->
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().monthValue
} ?: currentDate.monthValue,
viewModel.datePickerState.selectedDateMillis?.let { millis ->
java.time.Instant.ofEpochMilli(millis).atZone(java.time.ZoneId.systemDefault()).toLocalDate().dayOfMonth
} ?: currentDate.dayOfMonth
)
userProfile = userProfile.copy(
date_of_birth = selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)
)
showDatePicker = false
}) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("Отмена")
}
}
) {
DatePicker(state = viewModel.datePickerState)
}
}
// Диалог запроса разрешения на контакты
if (showContactPermissionDialog) {
PermissionRequestDialog(
permissionText = "Для выбора экстренного контакта необходимо разрешение на доступ к контактам.",
onDismiss = { showContactPermissionDialog = false },
onConfirm = {
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
showContactPermissionDialog = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Редактирование профиля") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = "Назад")
}
},
actions = {
IconButton(onClick = {
viewModel.updateUserProfile(userProfile)
onNavigateBack()
}) {
Icon(Icons.Filled.Save, contentDescription = "Сохранить")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Раздел с основной информацией
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Личная информация",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
OutlinedTextField(
value = userProfile.bio ?: "",
onValueChange = { userProfile = userProfile.copy(bio = it) },
label = { Text("О себе") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
maxLines = 5
)
// Дата рождения
OutlinedTextField(
value = userProfile.date_of_birth ?: "",
onValueChange = {},
label = { Text("Дата рождения") },
modifier = Modifier.fillMaxWidth(),
readOnly = true,
trailingIcon = {
IconButton(onClick = { showDatePicker = true }) {
Icon(Icons.Filled.DateRange, contentDescription = "Выбрать дату")
}
}
)
}
}
// Раздел с экстренными контактами
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Экстренные контакты",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
// Первый экстренный контакт
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Первый контакт",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
text = if (userProfile.emergency_contact_1_name.isNullOrBlank()) "Не выбрано"
else "${userProfile.emergency_contact_1_name}\n${userProfile.emergency_contact_1_phone}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Button(
onClick = { checkAndRequestContactPermission(ContactType.EMERGENCY_1) },
modifier = Modifier.padding(start = 8.dp)
) {
Icon(Icons.Filled.Contacts, contentDescription = "Выбрать контакт")
Spacer(modifier = Modifier.width(8.dp))
Text("Выбрать")
}
}
// Второй экстренный контакт
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Второй контакт",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Text(
text = if (userProfile.emergency_contact_2_name.isNullOrBlank()) "Не выбрано"
else "${userProfile.emergency_contact_2_name}\n${userProfile.emergency_contact_2_phone}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Button(
onClick = { checkAndRequestContactPermission(ContactType.EMERGENCY_2) },
modifier = Modifier.padding(start = 8.dp)
) {
Icon(Icons.Filled.Contacts, contentDescription = "Выбрать контакт")
Spacer(modifier = Modifier.width(8.dp))
Text("Выбрать")
}
}
}
}
// Раздел с настройками уведомлений и доступа
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Настройки и разрешения",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
// Включение/выключение экстренных уведомлений
SwitchRow(
title = "Экстренные уведомления",
checked = userProfile.emergency_notifications_enabled ?: false,
onCheckedChange = {
userProfile = userProfile.copy(emergency_notifications_enabled = it)
}
)
// Включение/выключение обмена местоположением
SwitchRow(
title = "Обмен местоположением",
checked = userProfile.location_sharing_enabled ?: false,
onCheckedChange = {
userProfile = userProfile.copy(location_sharing_enabled = it)
}
)
// Включение/выключение push-уведомлений
SwitchRow(
title = "Push-уведомления",
checked = userProfile.push_notifications_enabled ?: false,
onCheckedChange = {
userProfile = userProfile.copy(push_notifications_enabled = it)
}
)
}
}
}
}
}
@Composable
fun SwitchRow(
title: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}
// Перечисление типов контактов
enum class ContactType {
NONE,
EMERGENCY_1,
EMERGENCY_2
}

View File

@@ -60,9 +60,9 @@ fun ProfileScreen(
var integrations by remember {
mutableStateOf(
listOf(
Integration("Google Fit", false),
Integration("FatSecret Proxy", true),
Integration("Wear OS", false)
Integration("Google Fit", true),
Integration("FatSecret Proxy", false),
Integration("Wear OS", true)
)
)
}
@@ -106,7 +106,7 @@ fun ProfileScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Цель воды (мл)",
text = "Цель воды в сутки (мл)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

View File

@@ -0,0 +1,85 @@
package kr.smartsoltech.wellshe.ui.profile.viewmodel
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kr.smartsoltech.wellshe.model.auth.UserProfile
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor() : ViewModel() {
private val _userProfile = MutableStateFlow<UserProfile?>(null)
val userProfile: StateFlow<UserProfile?> = _userProfile.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
@OptIn(ExperimentalMaterial3Api::class)
val datePickerState = DatePickerState(
initialSelectedDateMillis = LocalDate.now().minusYears(20).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
initialDisplayedMonthMillis = LocalDate.now().minusYears(20).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
yearRange = IntRange(1900, LocalDate.now().year),
initialDisplayMode = DisplayMode.Picker,
locale = java.util.Locale.getDefault()
)
init {
loadUserProfile()
}
fun loadUserProfile() {
viewModelScope.launch {
_isLoading.value = true
try {
// Здесь должен быть запрос к API или базе данных для получения профиля пользователя
// Пока используем заполнитель с фиктивными данными
_userProfile.value = UserProfile(
username = "user123",
email = "user@example.com",
firstName = "Иван",
lastName = "Иванов",
phone = "+7 (999) 123-45-67"
)
_error.value = null
} catch (e: Exception) {
_error.value = "Не удалось загрузить профиль: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun updateUserProfile(profile: UserProfile) {
viewModelScope.launch {
_isLoading.value = true
try {
// Здесь должен быть запрос к API или базе данных для обновления профиля
// Пока просто обновляем локальное состояние
_userProfile.value = profile
_error.value = null
} catch (e: Exception) {
_error.value = "Не удалось обновить профиль: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun clearError() {
_error.value = null
}
}

View File

@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB