diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt index 3c51846a..71aa83f4 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/MainActivity.kt @@ -48,7 +48,7 @@ import com.cmpe451.resq.utils.NavigationItem import com.cmpe451.resq.viewmodels.MapViewModel import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices - +import com.cmpe451.resq.ui.views.screens.MyRequestsScreen class MainActivity : ComponentActivity() { private val requestPermissionLauncher = @@ -153,6 +153,9 @@ fun NavGraph( composable(NavigationItem.Settings.route) { SettingsScreen(navController, appContext) } + composable(NavigationItem.MyRequestsScreen.route) { + MyRequestsScreen(navController, appContext) + } } } diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Need.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Need.kt index 5b0694ac..a82a7c44 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Need.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/data/models/Need.kt @@ -10,5 +10,7 @@ data class Need( val longitude: Double, val requestId: Int?, val status: String, - val createdDate: String -) \ No newline at end of file + val createdDate: String, + val size: String +) + 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 a38dc279..51e331a3 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 @@ -27,8 +27,8 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.* import java.time.LocalDate import java.time.format.DateTimeParseException - - +import retrofit2.Call +import retrofit2.Callback interface CategoryTreeNodeService { @GET("categorytreenode/getMainCategories") suspend fun getMainCategories( @@ -68,8 +68,13 @@ interface NeedService { ): Call> -} + @GET("need/viewNeedsByUserId") + fun viewNeedsByUserId( + @Query("userId") userId: Int, + @Header("Authorization") jwtToken: String, + ): Call> +} interface AuthService { @POST("auth/signin") suspend fun login(@Body requestBody: LoginRequestBody): Response @@ -210,6 +215,27 @@ class ResqService(appContext: Context) { }) } + fun viewNeedsByUserId( + onSuccess: (List) -> Unit, + onError: (Throwable) -> Unit + ) { + val token = userSessionManager.getUserToken() ?: "" + val userId = userSessionManager.getUserId() + needService.viewNeedsByUserId(userId = userId, "Bearer $token").enqueue(object : + Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.isSuccessful) { + response.body()?.let { onSuccess(it) } + } else { + onError(RuntimeException("Response not successful")) + } + } + override fun onFailure(call: Call>, t: Throwable) { + onError(t) + } + }) + } + // Auth methods suspend fun login(request: LoginRequestBody): Response = authService.login(request) suspend fun register(request: RegisterRequestBody): Response = authService.register(request) diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/MyRequestsScreen.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/MyRequestsScreen.kt new file mode 100644 index 00000000..116c6d6f --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/ui/views/screens/MyRequestsScreen.kt @@ -0,0 +1,139 @@ +package com.cmpe451.resq.ui.views.screens +import android.content.Context +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.cmpe451.resq.data.models.Need +import com.cmpe451.resq.ui.theme.RequestColor +import com.cmpe451.resq.viewmodels.MyRequestsViewModel +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import java.text.SimpleDateFormat +import java.util.Locale + +fun convertToReadableDate(dateStr: String): String { + val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.getDefault()) + val outputFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()) + + return try { + val date = inputFormat.parse(dateStr) + date?.let { outputFormat.format(it) } ?: "Unknown Date" + } catch (e: Exception) { + "Invalid Date" + } +} + +@Composable +fun MyRequestsScreen(navController: NavController, appContext: Context) { + val viewModel: MyRequestsViewModel = viewModel() + val needs by viewModel.needs + val scrollState = rememberScrollState() + // A side effect to load the needs when the composable enters the composition + LaunchedEffect(key1 = true) { + viewModel.getNeeds(appContext) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "My Requests", color = RequestColor) }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back") + } + }, + backgroundColor = Color.White, + elevation = 4.dp, + contentColor = Color.Black + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.dp)) + needs.forEachIndexed { index, need -> + RequestCard(viewModel, requestNumber = index + 1, request = need) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +fun RequestCard(viewModel: MyRequestsViewModel,requestNumber: Int, request: Need) { + var isSelected by remember { mutableStateOf(false) } + val categoryName = viewModel.getCategoryName(request.categoryTreeId.toInt()) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clickable { isSelected = !isSelected }, + elevation = if (isSelected) 4.dp else 0.dp, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(width = if (isSelected) 2.dp else 0.dp, color = RequestColor) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "#$requestNumber", + style = MaterialTheme.typography.body1, + color = Color(0xFF007BFF) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = categoryName, + style = MaterialTheme.typography.body1, + color = RequestColor + ) + } + if (isSelected) { + Text("Description: ${request.description}") + Text("Quantity: ${request.quantity}") + Text("Latitude: ${request.latitude}") + Text("Longitude: ${request.longitude}") + Text("Status: ${request.status}") + Text("Created Date: ${convertToReadableDate(request.createdDate)}") + } + } + } +} 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 45c31b0b..d4177da4 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 @@ -698,8 +698,8 @@ fun FacilitatorProfileButtons(navController: NavController) { ) { ProfileButton( color = RequestColor, - text = "My Request", - route = "request", + text = "My Requests", + route = "myRequests", navController = navController ) Spacer(modifier = Modifier.width(30.dp)) diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/utils/NavigationItem.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/utils/NavigationItem.kt index 66d0683a..cb0a03f3 100644 --- a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/utils/NavigationItem.kt +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/utils/NavigationItem.kt @@ -10,6 +10,7 @@ enum class NavigationItem(val route: String) { Request(route = "request"), Resource(route = "resource"), Task(route = "task"), + MyRequestsScreen(route = "myRequests"), OngoingTasks(route = "ongoingTasks"); companion object { diff --git a/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/MyRequestsViewModel.kt b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/MyRequestsViewModel.kt new file mode 100644 index 00000000..fbe9858f --- /dev/null +++ b/resq/mobile/ResQ/app/src/main/java/com/cmpe451/resq/viewmodels/MyRequestsViewModel.kt @@ -0,0 +1,52 @@ +package com.cmpe451.resq.viewmodels + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.cmpe451.resq.data.models.CategoryTreeNode +import com.cmpe451.resq.data.models.Need +import com.cmpe451.resq.data.remote.ResqService +import kotlinx.coroutines.launch +import android.content.Context + +class MyRequestsViewModel : ViewModel() { + val needs = mutableStateOf>(emptyList()) + private val _categoryTree = mutableStateOf>(emptyList()) + suspend fun getNeeds(appContext: Context) { + val api = ResqService(appContext) + api.viewNeedsByUserId( + onSuccess = { needList -> + needs.value = needList + fetchCategoryTree(appContext) + }, + onError = { error -> + // Handle error + } + ) + } + + private fun fetchCategoryTree(context: Context) { + val api = ResqService(context) + viewModelScope.launch { + val response = api.getMainCategories() // Assuming this method exists and fetches the entire category tree + if (response.isSuccessful) { + _categoryTree.value = response.body() ?: emptyList() + } else { + // Handle error + } + } + } + + fun getCategoryName(categoryId: Int): String { + return findCategoryName(_categoryTree.value, categoryId) + } + + private fun findCategoryName(categoryList: List, categoryId: Int): String { + for (category in categoryList) { + if (category.id == categoryId) return category.data + val foundName = findCategoryName(category.children, categoryId) + if (foundName.isNotEmpty() && foundName != "Unknown Category") return foundName + } + return "Unknown Category" + } +}