diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7068dff..4d02903 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy to EC2 on: push: branches: - - fcm + - member-setting - prod paths: - 'backend/**' diff --git a/backend/src/controllers/group.controller.ts b/backend/src/controllers/group.controller.ts index 7add697..2e656f3 100644 --- a/backend/src/controllers/group.controller.ts +++ b/backend/src/controllers/group.controller.ts @@ -140,7 +140,7 @@ export class GroupController { } } - async updateGroupByJoinCode( + async joinGroupByJoinCode( req: Request, res: Response, next: NextFunction @@ -210,6 +210,40 @@ export class GroupController { } } + async updateGroupByJoinCode( + req: Request, + res: Response, + next: NextFunction + ) { + try { + const {joinCode, expectedPeople, groupMemberIds, meetingTime} = req.body; + const updatedGroup = await groupModel.updateGroupByJoinCode(joinCode, + {joinCode, expectedPeople, + groupMemberIds: groupMemberIds || [], meetingTime}); + + if (!updatedGroup) { + return res.status(404).json({ + message: 'Group not found', + }); + } + + res.status(200).json({ + message: 'Group info updated successfully', + data: { group: updatedGroup }, + }); + } catch (error) { + logger.error('Failed to update group info:', error); + + if (error instanceof Error) { + return res.status(500).json({ + message: error.message || 'Failed to update group info', + }); + } + + next(error); + } + } + async deleteGroupByJoinCode( req: Request<{joinCode: string}>, res: Response, diff --git a/backend/src/routes/group.routes.ts b/backend/src/routes/group.routes.ts index 33188f7..be05938 100644 --- a/backend/src/routes/group.routes.ts +++ b/backend/src/routes/group.routes.ts @@ -20,6 +20,12 @@ router.post( //have seperate endpoint for updating? ); router.post( //have seperate endpoint for updating? + '/join', + validateBody(updateGroupSchema), // Validate the request body + groupController.joinGroupByJoinCode.bind(groupController) +); + +router.post( '/update', validateBody(updateGroupSchema), // Validate the request body groupController.updateGroupByJoinCode.bind(groupController) @@ -35,7 +41,6 @@ router.get( groupController.getMidpointByJoinCode.bind(groupController) ); - router.post( '/leave/:joinCode', // Define the route parameter groupController.leaveGroup.bind(groupController) // Bind the controller method diff --git a/backend/src/types/group.types.ts b/backend/src/types/group.types.ts index 616033d..8553a22 100644 --- a/backend/src/types/group.types.ts +++ b/backend/src/types/group.types.ts @@ -77,6 +77,7 @@ export const updateGroupSchema = z.object({ address: addressSchema.optional(), transitType: transitTypeSchema.optional() })).optional(), + meetingTime: z.string().optional(), midpoint: z.string().default('').optional() }); diff --git a/frontend/app/src/main/java/com/cpen321/squadup/data/remote/api/GroupInterface.kt b/frontend/app/src/main/java/com/cpen321/squadup/data/remote/api/GroupInterface.kt index a8cb0a5..5cea792 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/data/remote/api/GroupInterface.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/data/remote/api/GroupInterface.kt @@ -42,12 +42,18 @@ interface GroupInterface { @retrofit2.http.Path("joinCode") joinCode: String ): Response> - @POST("group/update") + @POST("group/join") suspend fun joinGroup( @Header("Authorization") authHeader: String, @Body request: UpdateGroupRequest ): Response + @POST("group/update") + suspend fun updateGroup( + @Header("Authorization") authHeader: String, + @Body request: UpdateGroupRequest + ): Response + @DELETE("group/delete/{joinCode}") suspend fun deleteGroup( @Header("Authorization") authHeader: String, diff --git a/frontend/app/src/main/java/com/cpen321/squadup/data/remote/dto/GroupModels.kt b/frontend/app/src/main/java/com/cpen321/squadup/data/remote/dto/GroupModels.kt index 6307b8c..f51fdd8 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/data/remote/dto/GroupModels.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/data/remote/dto/GroupModels.kt @@ -10,7 +10,8 @@ data class CreateGroupRequest( data class UpdateGroupRequest( val joinCode: String? = null, val expectedPeople: Number? = null, - val groupMemberIds: List?= null + val groupMemberIds: List?= null, + val meetingTime: String? = null ) data class LeaveGroupRequest( diff --git a/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepository.kt b/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepository.kt index 6a61e4e..80d254c 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepository.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepository.kt @@ -19,8 +19,14 @@ interface GroupRepository { suspend fun joinGroup( joinCode: String, expectedPeople: Number, - updatedMembers: List + updatedMembers: List, ): Result //add exp members later + suspend fun updateGroup( + joinCode: String, + expectedPeople: Number?, + updatedMembers: List?, + meetingTime: String? + ): Result suspend fun getMidpointByJoinCode(joinCode: String): Result suspend fun leaveGroup( diff --git a/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepositoryImpl.kt b/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepositoryImpl.kt index 5f066f0..5fec461 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepositoryImpl.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/data/repository/GroupRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.cpen321.squadup.data.repository +import android.health.connect.ReadRecordsRequestUsingIds import android.util.Log import com.cpen321.squadup.data.local.preferences.TokenManager import com.cpen321.squadup.data.remote.api.GroupInterface @@ -144,6 +145,38 @@ class GroupRepositoryImpl @Inject constructor( } } + override suspend fun updateGroup( + joinCode: String, + expectedPeople: Number?, + updatedMembers: List?, + meetingTime: String? + ): Result { + return try { + val authToken = tokenManager.getToken() ?: "" + val request = UpdateGroupRequest( + joinCode = joinCode, + expectedPeople = expectedPeople, + groupMemberIds = updatedMembers, + meetingTime = meetingTime + ) + Log.d(TAG, "GroupRepImpl updateGroupRequest ${request}") + val response = groupInterface.updateGroup( + authHeader = "Bearer $authToken", + request = request + ) + Log.d(TAG, "GroupRepImpl updateGroupRequest response ${response}") + if (response.isSuccessful) { + Result.success(Unit) + } else { + val errorBodyString = response.errorBody()?.string() + val errorMessage = parseErrorMessage(errorBodyString, "Failed to join group.") + Result.failure(Exception(errorMessage)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + override suspend fun getMidpointByJoinCode(joinCode: String): Result { return try { val authToken = tokenManager.getToken() ?: "" diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/Navigation.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/Navigation.kt index 75821d3..e5c9112 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/Navigation.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/Navigation.kt @@ -25,6 +25,7 @@ import com.cpen321.squadup.ui.screens.ProfileScreenActions import com.cpen321.squadup.ui.screens.ProfileCompletionScreen import com.cpen321.squadup.ui.screens.ProfileScreen import com.cpen321.squadup.ui.screens.JoinGroupScreen +import com.cpen321.squadup.ui.screens.MemberSettingsScreen import com.cpen321.squadup.ui.viewmodels.AuthViewModel import com.cpen321.squadup.ui.viewmodels.MainViewModel import com.cpen321.squadup.ui.viewmodels.NavigationViewModel @@ -43,7 +44,8 @@ object NavRoutes { const val PROFILE_COMPLETION = "profile_completion" const val GROUP_DETAILS = "group_details" const val GROUP_LIST = "group_list" - const val JOIN_GROUP = "join_group" + const val JOIN_GROUP = "join_group" + const val MEMBER_SETTINGS = "member_settings" } @Composable @@ -235,7 +237,8 @@ private fun AppNavHost( actions = ProfileScreenActions( onBackClick = { navigationStateManager.navigateBack() }, onManageProfileClick = { navigationStateManager.navigateToManageProfile() }, - onAccountDeleted = { navigationStateManager.handleAccountDeletion() } + onAccountDeleted = { navigationStateManager.handleAccountDeletion() }, + onAccountLogOut = { navigationStateManager.handleAccountLogOut() } ) ) } @@ -290,14 +293,28 @@ private fun AppNavHost( } } + composable("${NavRoutes.MEMBER_SETTINGS}/{joinCode}") { backStackEntry -> + val joinCode = backStackEntry.arguments?.getString("joinCode") ?: "" + val group = mainViewModel.getGroupById(joinCode) + + group?.let { + MemberSettingsScreen( + navController = navController, + group = group, + groupViewModel = groupViewModel, + profileViewModel = profileViewModel + ) + } + } + composable(NavRoutes.JOIN_GROUP) { JoinGroupScreen( - navController = navController, - mainViewModel = mainViewModel, + navController = navController, + mainViewModel = mainViewModel, profileViewModel = profileViewModel, ) - } + } } } diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/NavigationStateManager.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/NavigationStateManager.kt index 6e110ff..a57ccdf 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/NavigationStateManager.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/navigation/NavigationStateManager.kt @@ -188,6 +188,20 @@ class NavigationStateManager @Inject constructor() { navigateToAuthWithMessage("Account deleted successfully!") } + /** + * Handle account deletion + */ + fun handleAccountLogOut() { + _navigationState.value = _navigationState.value.copy(isNavigating = true) + + updateAuthenticationState( + isAuthenticated = false, + needsProfileCompletion = false, + isLoading = false + ) + navigateToAuthWithMessage("Account logged out successfully!") + } + /** * Handle profile completion */ diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupDetailsScreen.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupDetailsScreen.kt index ea85a7e..07a2e27 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupDetailsScreen.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupDetailsScreen.kt @@ -117,7 +117,7 @@ fun GroupDetailsScreen( } }, navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { + IconButton(onClick = { navController.navigate(NavRoutes.MAIN) }){ Icon(Icons.Default.ArrowBack, contentDescription = "Back") } } @@ -250,7 +250,7 @@ fun GroupDetailsScreen( // See Details button Button( - onClick = { // Suppose you already have the group object + onClick = { navController.navigate("${NavRoutes.GROUP_LIST}/${group.joinCode}") }, modifier = Modifier diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupListScreen.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupListScreen.kt index 484224e..f84e3d1 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupListScreen.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/GroupListScreen.kt @@ -66,7 +66,9 @@ fun GroupListScreen( ) }, navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { + IconButton(onClick = { + navController.navigate("${NavRoutes.GROUP_DETAILS}/${group.joinCode}") + },) { Icon(Icons.Default.ArrowBack, contentDescription = "Back") } }, @@ -111,7 +113,7 @@ fun GroupListScreen( ) NavigationBarItem( selected = false, - onClick = { navController.navigate(NavRoutes.PROFILE) }, + onClick = {navController.navigate("${NavRoutes.MEMBER_SETTINGS}/${group.joinCode}") }, icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") }, label = { Text("Settings") } ) diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/JoinGroupScreen.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/JoinGroupScreen.kt index ae1fc0e..1eb65ab 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/JoinGroupScreen.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/JoinGroupScreen.kt @@ -15,13 +15,6 @@ import com.cpen321.squadup.data.remote.dto.GroupUser import androidx.hilt.navigation.compose.hiltViewModel import androidx.compose.runtime.mutableStateOf import androidx.compose.material3.TextField -import com.cpen321.squadup.data.remote.dto.Address -import com.cpen321.squadup.data.remote.dto.TransitType -import androidx.compose.ui.res.stringResource -import com.cpen321.squadup.R -import com.cpen321.squadup.data.remote.dto.GroupDataDetailed -import com.cpen321.squadup.ui.components.AddressPicker -import com.cpen321.squadup.ui.viewmodels.AddressPickerViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -32,17 +25,8 @@ fun JoinGroupScreen( ) { var joinCode by remember { mutableStateOf("") } var joinGroupMessage by remember { mutableStateOf(null) } - var groupExists by remember { mutableStateOf(false) } - var groupMeetingTime by remember { mutableStateOf(null) } - var groupName by remember { mutableStateOf(null) } - var address by remember { mutableStateOf(null) } - var transitType by remember { mutableStateOf(null) } val profileUiState by profileViewModel.uiState.collectAsState() - LaunchedEffect(Unit) { - transitType = profileUiState.user?.transitType - } - LaunchedEffect(Unit) { profileViewModel.loadProfile() } @@ -67,118 +51,49 @@ fun JoinGroupScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // Join Code Input TextField( value = joinCode, - onValueChange = { joinCode = it }, + onValueChange = {joinCode = it}, label = { Text("Enter Join Code") }, modifier = Modifier.fillMaxWidth(), singleLine = true, maxLines = 1 ) Spacer(modifier = Modifier.height(16.dp)) - - // Check if the group exists Button( onClick = { + // Handle join group logic here if (joinCode.length == 6) { - mainViewModel.checkGroupExists( - joinCode = joinCode, - onSuccess = { group -> - groupExists = true - groupName = group.groupName - groupMeetingTime = group.meetingTime // Fetch meeting time - joinGroupMessage = "" - }, - onError = { - groupExists = false - joinGroupMessage = "Group not found. Please check the join code." - } - ) - } else { - joinGroupMessage = "Invalid join code!" - } - }, - enabled = joinCode.length == 6 - ) { - Text("Check Group") - } - Spacer(modifier = Modifier.height(16.dp)) - - // Display Group Meeting Time and Prompt (only if group exists) - if (groupExists) { - groupName?.let { - Text( - text = "$it", - style = MaterialTheme.typography.titleLarge - ) - Spacer(modifier = Modifier.height(16.dp)) - } - - groupMeetingTime?.let { - Text( - text = "Meeting Date & Time: $it", - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(16.dp)) - } - - Text( - text = "Please enter your location prior to event and your preferred mode of transit.", - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(16.dp)) - - // Address Input - val addressPickerViewModel: AddressPickerViewModel = hiltViewModel() - AddressPicker( - viewModel = addressPickerViewModel, - initialValue = null, //TODO get default from profile - onAddressSelected = { selectedAddressObject -> - address = selectedAddressObject - } - ) - Spacer(modifier = Modifier.height(16.dp)) - - // Transit Type Drop-Down - TransitTypeInputField( - transitType = transitType, - isEnabled = true, - onTransitTypeChange = { transitType = it } - ) - Spacer(modifier = Modifier.height(16.dp)) - - // Join Group Button - Button( - onClick = { val currentUser = GroupUser( - id = profileUiState.user!!._id, - name = profileUiState.user!!.name, - email = profileUiState.user!!.email, - address = address!!, - transitType = transitType + id = profileUiState!!.user!!._id, + name = profileUiState!!.user!!.name, + email = profileUiState!!.user!!.email, + address = profileUiState!!.user!!.address, + transitType = profileUiState!!.user!!.transitType ) + val joinCodeString: String = joinCode mainViewModel.joinGroup( - joinCode = joinCode, + joinCode = joinCodeString, currentUser = currentUser, onSuccess = { message -> - joinGroupMessage = message + joinGroupMessage= message mainViewModel.fetchGroups() // Refresh groups after joining - navController.popBackStack() // Navigate back after joining + joinGroupMessage = "Successfully joined group!" }, onError = { error -> joinGroupMessage = error + joinGroupMessage = "Error joining group." } - ) - }, - enabled = address != null && transitType != null - ) { - Text("Join Group") - } + ) + } else { + joinGroupMessage = "Invalid join code!" + } + }, + enabled = joinCode.length == 6 + ) { + Text("Join Group") } - - // Display Join Group Message joinGroupMessage?.let { Spacer(modifier = Modifier.height(8.dp)) Text( @@ -188,52 +103,4 @@ fun JoinGroupScreen( } } } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TransitTypeInputField( - transitType: TransitType?, - isEnabled: Boolean, - onTransitTypeChange: (TransitType?) -> Unit, - modifier: Modifier = Modifier -) { - val transitOptions = TransitType.entries.toList() // Use enum - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded && isEnabled, - onExpandedChange = { if (isEnabled) expanded = !expanded }, - modifier = modifier - ) { - OutlinedTextField( - value = transitType?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(R.string.transit_type)) }, - placeholder = { Text(stringResource(R.string.transit_type_placeholder)) }, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) - }, - modifier = Modifier - .menuAnchor() - .fillMaxWidth(), - enabled = isEnabled - ) - - ExposedDropdownMenu( - expanded = expanded && isEnabled, - onDismissRequest = { expanded = false } - ) { - transitOptions.forEach { option -> - DropdownMenuItem( - text = { Text(option.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - onTransitTypeChange(option) // pass enum, not string - expanded = false - } - ) - } - } - } } \ No newline at end of file diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/MemberSettingsScreen.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/MemberSettingsScreen.kt new file mode 100644 index 0000000..d931c0c --- /dev/null +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/MemberSettingsScreen.kt @@ -0,0 +1,194 @@ +package com.cpen321.squadup.ui.screens + +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.AccountCircle +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.cpen321.squadup.data.remote.dto.Address +import com.cpen321.squadup.data.remote.dto.GroupDataDetailed +import com.cpen321.squadup.data.remote.dto.TransitType +import com.cpen321.squadup.data.remote.dto.GroupUser +import com.cpen321.squadup.ui.viewmodels.GroupViewModel +import com.cpen321.squadup.ui.components.AddressPicker +import com.cpen321.squadup.ui.navigation.NavRoutes +import com.cpen321.squadup.ui.viewmodels.MainViewModel +import com.cpen321.squadup.ui.viewmodels.ProfileViewModel +import com.cpen321.squadup.ui.viewmodels.AddressPickerViewModel +import androidx.compose.ui.platform.LocalContext +import android.app.TimePickerDialog +import android.app.DatePickerDialog +import android.widget.Toast +import java.util.* + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MemberSettingsScreen( + navController: NavController, + group: GroupDataDetailed, + profileViewModel: ProfileViewModel = hiltViewModel(), + groupViewModel: GroupViewModel, +) { + val profileUiState by profileViewModel.uiState.collectAsState() + val currentUser = profileUiState.user + val currentUserId = profileUiState.user?._id + val context = LocalContext.current + + // Form state + var address by remember { mutableStateOf(currentUser?.address) } + var transitType by remember { mutableStateOf(currentUser?.transitType) } + + // Leader-only fields + var meetingTime by remember { mutableStateOf(group.meetingTime ?: "") } + var expectedPeople by remember { mutableStateOf(group.expectedPeople?.toString() ?: "") } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text( + text = "Member Settings", + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold) + ) + } + }, + navigationIcon = { + IconButton(onClick = { + navController.navigate("${NavRoutes.GROUP_DETAILS}/${group.joinCode}") + }) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + } + ) + }, + bottomBar = { + NavigationBar { + NavigationBarItem( + selected = false, + onClick = {navController.navigate("${NavRoutes.GROUP_LIST}/${group.joinCode}")}, + icon = { Icon(Icons.Default.AccountCircle, contentDescription = "Squads") }, + label = { Text("Squads") } + ) + NavigationBarItem( + selected = true, + onClick = { }, + icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") }, + label = { Text("Settings") } + ) + } + }, + content = { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val addressPickerViewModel: AddressPickerViewModel = hiltViewModel() + AddressPicker( + viewModel = addressPickerViewModel, + initialValue = address, + onAddressSelected = { address = it } + ) + + TransitTypeDropdown( + selectedType = transitType, + onTypeSelected = { transitType = it } + ) + + // Leader-only: allow updating meeting time and expected people + if (group.groupLeaderId?.id == currentUserId) { + val context = LocalContext.current + val calendar = Calendar.getInstance() + + // Display-only field for meeting time + OutlinedTextField( + value = meetingTime, + onValueChange = {}, + label = { Text("Meeting Time") }, + modifier = Modifier.fillMaxWidth(), + readOnly = true + ) + + // Button to open time picker + Button( + onClick = { + // Step 1: Pick Date + DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + val selectedDate = String.format("%04d-%02d-%02d", year, month + 1, dayOfMonth) + + // Step 2: Then pick Time + TimePickerDialog( + context, + { _, hourOfDay, minute -> + val selectedTime = String.format("%02d:%02d", hourOfDay, minute) + meetingTime = "${selectedDate}T${selectedTime}:00Z" + Toast.makeText(context, "Meeting set to: $meetingTime", Toast.LENGTH_SHORT).show() + }, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + true + ).show() + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ).show() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text( text = "Update Meeting Date & Time" ) + } + + OutlinedTextField( + value = expectedPeople, + onValueChange = { expectedPeople = it }, + label = { Text("Expected People") }, + modifier = Modifier.fillMaxWidth() + ) + } + + Button( + onClick = { + val updatedMembers = group.groupMemberIds?.map { member -> + if (member.id == currentUserId) { + member.copy(address = address, transitType = transitType) + } else member + } ?: emptyList() + + groupViewModel.updateMember( + joinCode = group.joinCode, + updatedMembers = updatedMembers, + meetingTime = meetingTime, + expectedPeople = expectedPeople.toIntOrNull() ?: 0, + onSuccess = { Toast.makeText(context, "Settings saved successfully!", Toast.LENGTH_SHORT).show() }, + onError = { Toast.makeText(context, "Error saving!", Toast.LENGTH_SHORT).show() } + ) + }, + enabled = address != null && transitType != null + ) { + Text("Save") + } + } + } + ) +} + + diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/ProfileScreen.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/ProfileScreen.kt index 59db1fc..ac87127 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/ProfileScreen.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/screens/ProfileScreen.kt @@ -47,7 +47,8 @@ private data class ProfileDialogState( data class ProfileScreenActions( val onBackClick: () -> Unit, val onManageProfileClick: () -> Unit, - val onAccountDeleted: () -> Unit + val onAccountDeleted: () -> Unit, + val onAccountLogOut: () -> Unit ) private data class ProfileScreenCallbacks( @@ -102,8 +103,8 @@ fun ProfileScreen( onSuccessMessageShown = profileViewModel::clearSuccessMessage, onErrorMessageShown = profileViewModel::clearError, onLogoutClick = { - authViewModel.handleLogout() // you’ll implement this in your AuthViewModel - actions.onAccountDeleted() // or navigate back to login + authViewModel.handleLogout() + actions.onAccountLogOut() }, ) ) diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/GroupViewModel.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/GroupViewModel.kt index 9a95ad6..1e72fff 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/GroupViewModel.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/GroupViewModel.kt @@ -39,6 +39,8 @@ class GroupViewModel @Inject constructor( val isGroupLeft: StateFlow = _isGroupLeft private val _isCalculatingMidpoint = MutableStateFlow(false) val isCalculatingMidpoint: StateFlow = _isCalculatingMidpoint + private val _MemberUpdated = MutableStateFlow(false) + val MemberUpdated: StateFlow = _MemberUpdated fun createGroup(groupName: String, meetingTime: String, groupLeaderId: GroupUser, expectedPeople: Number) { viewModelScope.launch { @@ -110,6 +112,36 @@ class GroupViewModel @Inject constructor( } } + fun updateMember( + joinCode: String, + expectedPeople: Number, + updatedMembers: List, + meetingTime: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit + ) { + viewModelScope.launch { + val fetchResult = groupRepository.getGroupByJoinCode(joinCode) + if (fetchResult.isSuccess) { + val joinResult = groupRepository.updateGroup( + joinCode = joinCode, + expectedPeople = expectedPeople, + updatedMembers = updatedMembers, + meetingTime = meetingTime + ) + if (joinResult.isSuccess) { + onSuccess("Successfully updated!") + } else { + val error = joinResult.exceptionOrNull()?.message ?: "Failed to update" + onError(error) + } + } else { + val error = fetchResult.exceptionOrNull()?.message ?: "Failed to update member settings" + onError(error) + } + } + } + fun resetGroupDeletedState() { _isGroupDeleted.value = false } diff --git a/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/MainViewModel.kt b/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/MainViewModel.kt index 07e15f3..2d75c54 100644 --- a/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/MainViewModel.kt +++ b/frontend/app/src/main/java/com/cpen321/squadup/ui/viewmodels/MainViewModel.kt @@ -120,6 +120,27 @@ class MainViewModel @Inject constructor( } fun getGroupById(groupId: String): GroupDataDetailed? { + viewModelScope.launch { + val result = groupRepository.getGroupByJoinCode(groupId) + if (result.isSuccess) { + val updatedGroup = result.getOrNull() + updatedGroup?.let { newGroup -> + val updatedGroups = _uiState.value.groups.map { existingGroup -> + if (existingGroup.joinCode == groupId) newGroup else existingGroup + } + + _uiState.value = _uiState.value.copy( + groups = updatedGroups, + successMessage = null, + errorMessage = null + ) + } + } else { + val error = result.exceptionOrNull()?.message ?: "Failed to fetch group" + _uiState.value = _uiState.value.copy(errorMessage = error) + Log.e(TAG, "MainViewModel fetchGroupByJoinCode error: $error") + } + } return _uiState.value.groups.find { it.joinCode == groupId } // Use joinCode directly } @@ -141,25 +162,4 @@ class MainViewModel @Inject constructor( } } } - - fun checkGroupExists( - joinCode: String, - onSuccess: (GroupDataDetailed) -> Unit, - onError: (String) -> Unit - ) { - viewModelScope.launch { - val result = groupRepository.getGroupByJoinCode(joinCode) - if (result.isSuccess) { - val group = result.getOrNull() - if (group != null) { - onSuccess(group) - } else { - onError("Group not found") - } - } else { - val error = result.exceptionOrNull()?.message ?: "Failed to check group" - onError(error) - } - } - } }