diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..bed416d Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/kr/smartsoltech/wellshe/model/auth/UserProfile.kt b/app/src/main/java/kr/smartsoltech/wellshe/model/auth/UserProfile.kt index 9278e03..26800eb 100644 --- a/app/src/main/java/kr/smartsoltech/wellshe/model/auth/UserProfile.kt +++ b/app/src/main/java/kr/smartsoltech/wellshe/model/auth/UserProfile.kt @@ -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 ) diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/auth/compose/RegisterScreen.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/auth/compose/RegisterScreen.kt index d835066..793d666 100644 --- a/app/src/main/java/kr/smartsoltech/wellshe/ui/auth/compose/RegisterScreen.kt +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/auth/compose/RegisterScreen.kt @@ -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(null) } - var usernameError by remember { mutableStateOf(null) } - var passwordError by remember { mutableStateOf(null) } - var confirmPasswordError by remember { mutableStateOf(null) } - var firstNameError by remember { mutableStateOf(null) } - var lastNameError by remember { mutableStateOf(null) } - var phoneError by remember { mutableStateOf(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 ) } } diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/components/PermissionRequestDialog.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/components/PermissionRequestDialog.kt new file mode 100644 index 0000000..1b52532 --- /dev/null +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/components/PermissionRequestDialog.kt @@ -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("Отмена") + } + } + ) +} diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/navigation/AppNavGraph.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/navigation/AppNavGraph.kt index 8fb465a..d1a1411 100644 --- a/app/src/main/java/kr/smartsoltech/wellshe/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/navigation/AppNavGraph.kt @@ -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 } + } } ) } diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileEditScreen.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileEditScreen.kt new file mode 100644 index 0000000..40be1cd --- /dev/null +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileEditScreen.kt @@ -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 +} diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileScreen.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileScreen.kt index d25e615..0bbeeb8 100644 --- a/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/ProfileScreen.kt @@ -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 ) diff --git a/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/viewmodel/ProfileViewModel.kt b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/viewmodel/ProfileViewModel.kt new file mode 100644 index 0000000..f8db780 --- /dev/null +++ b/app/src/main/java/kr/smartsoltech/wellshe/ui/profile/viewmodel/ProfileViewModel.kt @@ -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(null) + val userProfile: StateFlow = _userProfile.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _error = MutableStateFlow(null) + val error: StateFlow = _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 + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..ca3826a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..6635d0a 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a41b32b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..6635d0a 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..b0aab0c 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..8d89d7f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..b0aab0c 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..b7e41a4 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..70417dd Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..b7e41a4 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..09ffce9 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..30a64e8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..09ffce9 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..d397c69 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f049f69 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..d397c69 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file