diff --git a/resq/mobile/ResQ/app/build.gradle.kts b/resq/mobile/ResQ/app/build.gradle.kts index 4ab1bd7c..21a0806d 100644 --- a/resq/mobile/ResQ/app/build.gradle.kts +++ b/resq/mobile/ResQ/app/build.gradle.kts @@ -101,6 +101,12 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") + + // Coil + implementation("io.coil-kt:coil-compose:2.4.0") + +} + } secrets{ @@ -108,4 +114,5 @@ secrets{ ignoreList.add("keyToIgnore") // Ignore the key "keyToIgnore" ignoreList.add("sdk.*") // Ignore all keys matching the regexp "sdk.*" -} \ No newline at end of file +} + diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Profile.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Profile.kt index 939051bd..11bab59d 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Profile.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Profile.kt @@ -3,25 +3,33 @@ package com.cmpe451.resq.data.models data class ProfileData( var name: String?, var surname: String?, - var email: String?, - var roles: List?, - var selectedRole: String?, + var birthdate: String?, + var gender:String?, + var bloodType: String?, var phoneNumber: String?, var country: String?, var city: String?, var state: String?, - var bloodType: String?, - var weight: String?, + var weight: Int?, + var height: Int?, + val emailConfirmed: Boolean? = false, + val privacyPolicyAccepted: Boolean? = false, +) + + +data class UserInfoRequest( + var name: String?, + var surname: String?, + var birthdate: String?, var gender:String?, - var height: String?, - var year: String?, - var month: String?, - var day: String? + var bloodType: String?, + var phoneNumber: String?, + var country: String?, + var city: String?, + var state: String?, + var weight: Int?, + var height: Int?, + val emailConfirmed: Boolean? = false, + val privacyPolicyAccepted: Boolean? = false, ) -data class UserInfoResponse( - val name: String, - val surname: String, - val email: String, - val roles: List -) \ No newline at end of file diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt index 33aefaf5..1aeac6be 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/remote/ResqService.kt @@ -14,7 +14,7 @@ import com.cmpe451.resq.data.models.Need import com.cmpe451.resq.data.models.ProfileData import com.cmpe451.resq.data.models.RegisterRequestBody import com.cmpe451.resq.data.models.UserInfoRequest -import com.cmpe451.resq.data.models.UserInfo + import okhttp3.ResponseBody import retrofit2.Response import retrofit2.Retrofit @@ -49,6 +49,7 @@ interface NeedService { ): Response + @GET("need/filterByDistance") fun filterNeedByDistance( @Query("latitude") latitude: Double, @@ -69,12 +70,15 @@ interface AuthService { } interface ProfileService { - @GET("user/getUserInfo") + @GET("profile/getProfileInfo") suspend fun getUserInfo( @Query("userId") userId: Int, @Header("Authorization") jwtToken: String, @Header("X-Selected-Role") role: String - ): Response + + ): Response + + @POST("user/requestRole") suspend fun selectRole( @@ -85,14 +89,14 @@ interface ProfileService { ): Response + @POST("profile/updateProfile") suspend fun updateProfile( @Query("userId") userId: Int, @Header("Authorization") jwtToken: String, @Header("X-Selected-Role") role: String, @Body request: UserInfoRequest - ): Response - + ): Response } class ResqService(appContext: Context) { @@ -175,17 +179,16 @@ class ResqService(appContext: Context) { if (birthDate.isNullOrBlank()) { return null } - - try { + return try { val date = LocalDate.parse(birthDate) val year = date.year.toString() val month = date.monthValue.toString() val day = date.dayOfMonth.toString() - return Triple(year, month, day) + Triple(year, month, day) } catch (e: DateTimeParseException) { //TO DO Handle parsing error if needed - return null + null } } @@ -202,43 +205,48 @@ class ResqService(appContext: Context) { role = selectedRole ) - val parsedDate = parseBirthDate(response.body()?.birth_date) - val (parsedYear, parsedMonth, parsedDay) = parsedDate ?: Triple("", "", "") - val profileData = ProfileData( - name = response.body()?.name, surname = response.body()?.surname, - email = response.body()?.email, - roles = response.body()?.roles, selectedRole = selectedRole, - year = parsedYear, month = parsedMonth, day = parsedDay, - city = response.body()?.city, country = response.body()?.country, - gender = response.body()?.gender, bloodType = response.body()?.bloodType, height = response.body()?.height.toString(), weight = response.body()?.weight.toString(), - phoneNumber = response.body()?.phoneNumber, state = response.body()?.state, - emailConfirmed = response.body()?.emailConfirmed, privacyPolicyAccepted = response.body()?.privacyPolicyAccepted - + return ProfileData( + name = response.body()?.name, + surname = response.body()?.surname, + city = response.body()?.city, + country = response.body()?.country, + gender = response.body()?.gender, + bloodType = response.body()?.bloodType, + height = response.body()?.height, + weight = response.body()?.weight, + phoneNumber = response.body()?.phoneNumber, + state = response.body()?.state, + emailConfirmed = response.body()?.emailConfirmed, + privacyPolicyAccepted = response.body()?.privacyPolicyAccepted, + birthdate = response.body()?.birthdate.toString(), ) - return profileData } @RequiresApi(Build.VERSION_CODES.O) - suspend fun updateUserData(profileData: ProfileData): Response{ + suspend fun updateUserData(profileData: ProfileData): Response { val token = userSessionManager.getUserToken() ?: "" val userId = userSessionManager.getUserId() val selectedRole = userSessionManager.getSelectedRole() ?: "" - val formattedBirthDate = profileData.getFormattedBirthDate() + val request = UserInfoRequest( - name = profileData.name ?: "", + name = profileData.name ?: "", surname = profileData.surname ?: "", - email = profileData.email ?: "", - roles = profileData.roles ?: listOf(), - birth_date = formattedBirthDate, + birthdate = null, country = profileData.country ?: "", city = profileData.city ?: "", state = profileData.state ?: "", bloodType = profileData.bloodType ?: "", - height = profileData.height?.toIntOrNull(), - weight = profileData.weight?.toIntOrNull(), + height = profileData.height, + weight = profileData.weight, gender = profileData.gender ?: "", phoneNumber = profileData.phoneNumber ?: "", ) + return profileService.updateProfile( + userId = userId, + jwtToken = "Bearer $token", + role = selectedRole, + request = request + ) val response = profileService.updateProfile( userId = userId, jwtToken = "Bearer $token", @@ -246,11 +254,8 @@ class ResqService(appContext: Context) { request = request ) return response - - - } - + suspend fun selectRole(requestedRole: String): Response { val userId = userSessionManager.getUserId() val token = userSessionManager.getUserToken() ?: "" @@ -262,7 +267,6 @@ class ResqService(appContext: Context) { jwtToken = "Bearer $token", role = requestedRole ) - return response } } diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/theme/Color.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/theme/Color.kt index 4043aa33..fa61ec70 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/theme/Color.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/theme/Color.kt @@ -16,4 +16,6 @@ val LightGreen = Color(0xff20df7f) val ResourceColor = Color(0xFF397FE7) val RequestColor = Color(0xFFB356AF) -val BackgroundColor = Color(0xFFEFEFEF) \ No newline at end of file +val MyTasksColor = Color(0XFFE7A139) +val OngoingTasksColor = Color(0xFFE16834) +val BackgroundColor = Color(0xFFEFEFEF) diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/ProfileScreen.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/ProfileScreen.kt index b04adc24..896349e3 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/ProfileScreen.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/ProfileScreen.kt @@ -1,6 +1,10 @@ package com.cmpe451.resq.ui.views.screens import android.content.Context +import android.net.Uri import android.os.Build +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -17,6 +21,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -49,10 +54,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType @@ -61,15 +69,64 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import coil.compose.rememberAsyncImagePainter +import com.cmpe451.resq.data.manager.UserSessionManager import com.cmpe451.resq.data.models.ProfileData +import com.cmpe451.resq.ui.theme.MyTasksColor +import com.cmpe451.resq.ui.theme.OngoingTasksColor +import com.cmpe451.resq.ui.theme.RequestColor +import com.cmpe451.resq.ui.theme.ResourceColor import com.cmpe451.resq.viewmodels.ProfileViewModel -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.KeyboardType -import com.cmpe451.resq.data.manager.UserSessionManager +@Composable +fun ProfileButton(color: Color, text:String, route: String, navController: NavController) { + Button( + onClick = { + if (route.isNotEmpty()){ + navController.navigate(route) + } + + }, + colors = ButtonDefaults.buttonColors(color), + modifier = Modifier + .size(170.dp, 60.dp) + ) { + Text(text = text) + } +} + +@Composable +fun ProfilePhoto() { + var imageUri = rememberSaveable { mutableStateOf("") } + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { imageUri.value = it.toString() } + } + if (imageUri.value.isEmpty()) { + Image( + Icons.Default.AccountCircle, + contentDescription = "User Profile", + modifier = Modifier + .size(150.dp) + .clickable { launcher.launch("image/*") }, + ) + } + else{ + val painter = rememberAsyncImagePainter(imageUri.value) + Image( + painter = painter, + contentDescription = "User Profile", + + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .clickable { launcher.launch("image/*") }, + contentScale = ContentScale.Crop + ) + } + // TO DO: Save imageUri.value to database, and retrieve it when the user logs in again. + // TO DO: Add deletion option +} import java.time.Year import androidx.compose.material.MaterialTheme.typography @@ -149,11 +206,11 @@ fun ProfileScreen(navController: NavController, appContext: Context) { Text("Loading...") } else -> { - val userRoles = profileData!!.roles + val userRoles = UserSessionManager.getInstance(appContext).getUserRoles() if (userRoles != null) { if (userRoles.contains("VICTIM") || userRoles.contains("RESPONDER") || userRoles.contains("FACILITATOR")) { + Profile(profileData = profileData!!, navController = navController, viewModel, appContext) - Profile(profileData = profileData!!, navController = navController, availableRoles, viewModel, appContext) } else { Text("Unknown Role") @@ -167,7 +224,7 @@ private fun String.isDigit() = filter { it.isDigit() } fun generateYears(start: Int, end: Int): List{ val years = mutableListOf() - for (i in start..end){ + for (i in end downTo start){ years.add(i.toString()) } return years @@ -198,49 +255,83 @@ fun generateDays(month: String): List{ @RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable -fun Profile(profileData:ProfileData, navController: NavController, availableRoles: List, viewModel: ProfileViewModel, appContext: Context) { +fun Profile(profileData:ProfileData, navController: NavController, viewModel: ProfileViewModel, appContext: Context) { val genders = listOf("Male", "Female") val bloodTypes = listOf("AB Rh+", "AB Rh-", "A Rh+", "A Rh-", "B Rh+", "B Rh-", "O Rh+", "O Rh-") - val years = generateYears(1900, Year.now().value) - val months = generateMonths() - var selectedRole = { mutableStateOf(profileData.selectedRole ?: "") } + val userSessionManager = UserSessionManager.getInstance(appContext) var name by remember { mutableStateOf(profileData.name ?: "") } var surname by remember { mutableStateOf(profileData.surname ?: "") } - var year by remember { mutableStateOf(profileData.year ?: "") } - var month by remember { mutableStateOf(profileData.month ?: "") } - var day by remember { mutableStateOf(profileData.day ?: "") } - var email by remember { mutableStateOf(profileData.email ?: "") } - var weight by remember { mutableStateOf(profileData.weight ?: "") } + var weight by remember { mutableStateOf(profileData.weight?.toString() ?: "") } var gender by remember { mutableStateOf(profileData.gender ?: "") } - var height by remember { mutableStateOf(profileData.height ?: "") } + var height by remember { mutableStateOf(profileData.height?.toString() ?: "") } var country by remember { mutableStateOf(profileData.country ?: "") } var city by remember { mutableStateOf(profileData.city ?: "") } var state by remember { mutableStateOf(profileData.state ?: "") } var phoneNumber by remember { mutableStateOf(profileData.phoneNumber ?: "") } var bloodType by remember { mutableStateOf(profileData.bloodType ?: "") } - var isEmailValid by remember { mutableStateOf(false) } - var isPhoneValid by remember { mutableStateOf(false) } + var isPhoneValid by remember { mutableStateOf(false) } var message by remember { mutableStateOf("") } val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - - ModalBottomSheetLayout( - sheetContent = { - BottomSheetContent( - availableRoles = availableRoles, - onRoleSelected = { selectedRole -> - viewModel.selectRole(selectedRole, appContext) - // Handle the role selection - coroutineScope.launch { modalBottomSheetState.hide() } + var profileColor = Color(0xFFFFFFFF) + + when (userSessionManager.getSelectedRole()) { + "VICTIM" -> { + profileColor = RequestColor + } + + "RESPONDER" -> { + profileColor = ResourceColor + } + + "FACILITATOR" -> { + profileColor = MyTasksColor + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .background(Color.White) + ) { + TopAppBar( + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back") } + }, + title = { + Text( + text = "Account", + style = TextStyle( + fontSize = 25.sp, + color = Color(0xFF224957), + textAlign = TextAlign.Center + ), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + }, + modifier = Modifier.fillMaxWidth() + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + ProfilePhoto() + } + Text( + text = "$name $surname", + modifier = Modifier.weight(1f), + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 18.sp + ) ) - }, - sheetState = modalBottomSheetState, - - ) { - Column( + } + Box( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) @@ -302,239 +393,184 @@ fun Profile(profileData:ProfileData, navController: NavController, availableRole .fillMaxWidth() .padding(16.dp) ) { - Row( + OutlinedTextField( + value = name, + onValueChange = { name = it.letterOrSpace() }, + label = { Text("First Name") }, + shape = RoundedCornerShape(15), modifier = Modifier - .fillMaxWidth() + .weight(1f) .padding(4.dp) - .background(Color.White) - ) { - name?.let { - OutlinedTextField( - value = it, - onValueChange = { name = it.letterOrSpace() }, - label = { Text("First Name") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text - ), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black - - ) - - ) - } + .background(Color.White), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text + ), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black + ) + ) + + OutlinedTextField( + value = surname, + onValueChange = { surname = it.letterOrSpace() }, + label = { Text("Last Name") }, + shape = RoundedCornerShape(15), + modifier = Modifier + .weight(1f) + .padding(4.dp) + .background(Color.White), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text + ), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black + ) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { - surname?.let { - OutlinedTextField( - value = it, - onValueChange = { surname = it.letterOrSpace() }, - label = { Text("Last Name") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text - ), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black - ) - ) - } + isPhoneValid = android.util.Patterns.PHONE.matcher(phoneNumber).matches() + OutlinedTextField( + value = phoneNumber, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Phone + ), + onValueChange = { + phoneNumber = it.isDigit() + isPhoneValid = android.util.Patterns.PHONE.matcher(it).matches() + }, - } + label = { Text("Phone Number") }, + shape = RoundedCornerShape(15), - Row( modifier = Modifier - .fillMaxWidth() + .weight(1f) + .padding(4.dp) + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, + focusedBorderColor = if (isPhoneValid) Color.Black else Color.Red + ) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + OutlinedTextField( + value = country, + onValueChange = { country = it.letterOrSpace() }, + label = { Text("Country") }, + shape = RoundedCornerShape(15), + modifier = Modifier + .weight(1f) .padding(4.dp) + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, - ) { - email?.let { - val isValidEmail = - android.util.Patterns.EMAIL_ADDRESS.matcher(it).matches() - isEmailValid = isValidEmail - - OutlinedTextField( - value = it, - onValueChange = { - email = it - isEmailValid = - android.util.Patterns.EMAIL_ADDRESS.matcher(it) - .matches() - }, - label = { Text("Email") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - focusedBorderColor = if (isEmailValid) Color.Black else Color.Red - ), - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Email - ) ) - } - } - Row( + ) + OutlinedTextField( + value = city, + onValueChange = { city = it.letterOrSpace() }, + label = { Text("City") }, + shape = RoundedCornerShape(15), modifier = Modifier - .fillMaxWidth() + .weight(1f) .padding(4.dp) - ) { - phoneNumber?.let { - OutlinedTextField( - value = it, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Phone - ), - onValueChange = { - phoneNumber = it.isDigit() - isPhoneValid = - android.util.Patterns.PHONE.matcher(it).matches() - }, - - label = { Text("Phone Number") }, - shape = RoundedCornerShape(15), - - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - focusedBorderColor = if (isPhoneValid) Color.Black else Color.Red - ) - ) - - } - } - Row( + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, + ) + ) + OutlinedTextField( + value = state, + onValueChange = { state = it.letterOrSpace() }, + label = { Text("State") }, + shape = RoundedCornerShape(15), modifier = Modifier - .fillMaxWidth() + .weight(1f) .padding(4.dp) - ) { - country?.let { - OutlinedTextField( - value = it, - onValueChange = { country = it.letterOrSpace() }, - label = { Text("Country") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - - ) - ) - } - city?.let { - OutlinedTextField( - value = it, - onValueChange = { city = it.letterOrSpace() }, - label = { Text("City") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - ) - ) - - } - state?.let { - OutlinedTextField( - value = it, - onValueChange = { state = it.letterOrSpace() }, - label = { Text("State") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - ) - ) - - - } - } - Row( + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, + ) + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + OutlinedTextField( + value = weight, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number + ), + onValueChange = { weight = it.isDigit() }, + label = { Text("Weight (kg)") }, + shape = RoundedCornerShape(15), modifier = Modifier - .fillMaxWidth() + .weight(1f) .padding(4.dp) - ) { - - weight?.let { - OutlinedTextField( - value = it, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number - ), - onValueChange = { weight = it.isDigit() }, - label = { Text("Weight (kg)") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black - ) - ) + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, + ) + ) + OutlinedTextField( + value = height, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number + ), + onValueChange = { height = it.isDigit() }, + label = { Text("Height (cm)") }, + shape = RoundedCornerShape(15), + modifier = Modifier + .weight(1f) + .padding(4.dp) + .background(Color.White), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = Color.White, + textColor = Color.Black, + cursorColor = Color.Black, + ) + ) + } + Row( + modifier = Modifier + .fillMaxWidth(), - } - height?.let { - OutlinedTextField( - value = it, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number - ), - onValueChange = { height = it.isDigit() }, - label = { Text("Height (cm)") }, - shape = RoundedCornerShape(15), - modifier = Modifier - .weight(1f) - .padding(4.dp) - .background(Color.White), - colors = TextFieldDefaults.outlinedTextFieldColors( - containerColor = Color.White, - textColor = Color.Black, - cursorColor = Color.Black, - ) - ) - } + ) { + Column(modifier = Modifier.weight(1f)) { + TextListSelectionWithColorChange( + items = genders, + selectedItem = gender, + onItemSelected = { gender = it }, + label = "Gender", + color = profileColor + ) } Row( modifier = Modifier @@ -553,78 +589,83 @@ fun Profile(profileData:ProfileData, navController: NavController, availableRole } } - - Column(modifier = Modifier.weight(1f)) { - bloodType?.let { - TextListSelectionWithColorChange( - items = bloodTypes, - selectedItem = bloodType, - onItemSelected = { bloodType = it }, - label = "Blood Type", - color = Color(0xFFB356AF) - ) - } - } - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(4.dp) - ) { - Column(modifier = Modifier.weight(1f)) { - year?.let { - TextListSelectionWithColorChange( - items = years, - selectedItem = year, - onItemSelected = { year = it }, - label = "Year", - color = Color(0xFFB356AF) - ) - } - } - Column(modifier = Modifier.weight(1f)) { - month?.let { - TextListSelectionWithColorChange( - items = months, - selectedItem = month, - onItemSelected = { month = it }, - label = "Month", - color = Color(0xFFB356AF) - ) - } - } - Column(modifier = Modifier.weight(1f)) { - val days = generateDays(month) - day?.let { - TextListSelectionWithColorChange( - items = days, - selectedItem = day, - onItemSelected = { day = it }, - label = "Day", - color = Color(0xFFB356AF) - ) - } - } + Column(modifier = Modifier.weight(1f)) { + TextListSelectionWithColorChange( + items = bloodTypes, + selectedItem = bloodType, + onItemSelected = { bloodType = it }, + label = "Blood Type", + color = profileColor + ) } } } } + Spacer(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + when (UserSessionManager.getInstance(appContext).getSelectedRole()) { + "VICTIM" -> { + VictimProfileButtons(navController = navController) + } + + "RESPONDER" -> { + ResponderProfileButtons(navController = navController) + } + + "FACILITATOR" -> { + FacilitatorProfileButtons(navController = navController) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + Row( + modifier = Modifier.align(Alignment.Start) ) { Row( modifier = Modifier.align(Alignment.Start) ) { Button( onClick = { - // @TO DO: Handle button click + if (!isPhoneValid) { + message = "Please check your phone number." + + } else { + // @TO DO Handle Save Details button click + Log.d("Save Details", "Save Details") + viewModel.updateProfile(appContext, ProfileData( + name = name, + surname = surname, + bloodType = bloodType, + country = country, + city = city, + state = state, + gender = gender.takeIf { it.isNotEmpty() } , + height = height.takeIf { it.isNotEmpty() }?.toInt(), + weight = weight.takeIf { it.isNotEmpty() }?.toInt(), + phoneNumber = phoneNumber, + birthdate = null + )) + if (viewModel.updateMessage.value != null) { + message = "Details saved successfully." + } + + else if (viewModel.errorMessage.value != null) { + message = viewModel.errorMessage.value!! + } + else{ + message = "Details saved successfully." + } + + } + }, colors = ButtonDefaults.buttonColors(Color(0xFFB356AF)), modifier = Modifier @@ -632,12 +673,14 @@ fun Profile(profileData:ProfileData, navController: NavController, availableRole ) { Text(text = "My Requests") } - } - Spacer(modifier = Modifier.height(20.dp)) - Row( - modifier = Modifier.align(Alignment.Start) - ) { - Row( + + Spacer(modifier = Modifier.width(30.dp)) + Button( + onClick = { + //@ TO DO Handle button click + }, + colors = ButtonDefaults.buttonColors(Color(0xFF224957)), + modifier = Modifier .fillMaxWidth() .padding(4.dp) @@ -699,6 +742,99 @@ fun Profile(profileData:ProfileData, navController: NavController, availableRole ) } + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) +} + +@Composable +fun FacilitatorProfileButtons(navController: NavController) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ProfileButton( + color = ResourceColor, + text = "My Resources", + route = "", + navController = navController + ) + Spacer(modifier = Modifier.width(30.dp)) + ProfileButton( + color = MyTasksColor, + text = "My Tasks", + route = "", + navController = navController + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ProfileButton( + color = RequestColor, + text = "My Request", + route = "", + navController = navController + ) + Spacer(modifier = Modifier.width(30.dp)) + ProfileButton( + color = OngoingTasksColor, + text = "Ongoing Tasks", + route = "OngoingTasks", + navController = navController + ) + } +} + + +@Composable +fun ResponderProfileButtons(navController: NavController) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ProfileButton( + color = ResourceColor, + text = "My Resources", + route = "", + navController = navController + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ProfileButton( + color = MyTasksColor, + text = "My Tasks", + route = "", + navController = navController + ) + } +} + +@Composable +fun VictimProfileButtons(navController: NavController) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + ) { + ProfileButton( + color = RequestColor, + text = "My Request", + route = "", + navController = navController + ) + } } @Composable diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/ProfileViewModel.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/ProfileViewModel.kt index 3144fd73..2d8eba50 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/ProfileViewModel.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/ProfileViewModel.kt @@ -1,8 +1,11 @@ package com.cmpe451.resq.viewmodels import android.content.Context +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -15,8 +18,14 @@ import kotlinx.coroutines.launch class ProfileViewModel() : ViewModel() { private var _profileData: MutableState = mutableStateOf(null) val profile get() = _profileData - private val errorMessage = MutableStateFlow(null) + private val _errorMessage = MutableStateFlow(null) + val errorMessage = _errorMessage + private val _updateMessage = mutableStateOf(null) + val updateMessage: State = _updateMessage + + + @RequiresApi(Build.VERSION_CODES.O) fun getUserData(appContext: Context) { val api = ResqService(appContext) @@ -30,16 +39,24 @@ class ProfileViewModel() : ViewModel() { } } - fun selectRole(role: String, appContext: Context) { - val userSessionManager: UserSessionManager = UserSessionManager.getInstance(appContext) + + @RequiresApi(Build.VERSION_CODES.O) + fun updateProfile(appContext: Context, profileData: ProfileData){ val api = ResqService(appContext) - val roles = userSessionManager.getUserRoles() viewModelScope.launch { try { - val response = api.selectRole(role) - Log.d("ProfileViewModel", "selectRole: $response") + val response = api.updateUserData(profileData) + + if (response.isSuccessful) { + _updateMessage.value = response.body() + _errorMessage.value = null + } + else { + _errorMessage.value = response.message() + ?: "Profile update failed. Please try again." + } } catch (e: Exception) { - errorMessage.value = e.message + _errorMessage.value = e.message ?: "Unexpected error occurred." } } }