Skip to content

Commit

Permalink
Merge pull request #303 from PeriodPals/feat/map/add-alerts-ui
Browse files Browse the repository at this point in the history
Add alerts UI to the map
  • Loading branch information
lazarinibruno authored Dec 20, 2024
2 parents 43fb86b + f4ac972 commit 4ec5870
Show file tree
Hide file tree
Showing 11 changed files with 910 additions and 127 deletions.
3 changes: 3 additions & 0 deletions app/src/main/java/com/android/periodpals/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ fun PeriodPalsApp(
gpsService,
authenticationViewModel,
alertViewModel,
locationViewModel,
chatViewModel,
userViewModel,
networkChangeListener,
navigationActions)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ class AlertViewModel(private val alertModelSupabase: AlertModelSupabase) : ViewM

private var _alertsWithinRadius = mutableStateOf<List<Alert>>(listOf())
val alertsWithinRadius: State<List<Alert>> = _alertsWithinRadius

private var alertFilter = mutableStateOf<(Alert) -> Boolean>({ true })

private var _filterAlerts = derivedStateOf {
_alertsWithinRadius.value.filter { alertFilter.value(it) }
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/android/periodpals/resources/C.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ object C {
const val SCREEN = "mapScreen"
const val MAP_VIEW_CONTAINER = "mapViewContainer"
const val MY_LOCATION_BUTTON = "mapButton"
const val BOTTOM_SHEET = "mapBottomSheet"
const val PROFILE_PICTURE = "profilePicture"
const val PROFILE_NAME = "profileName"
const val ALERT_LOCATION_TEXT = "locationText"
const val ALERT_TIME_TEXT = "alertTimeText"
const val ALERT_PRODUCT_ICON = "alertProductIcon"
const val ALERT_URGENCY_ICON = "alertUrgencyIcon"
const val ALERT_MESSAGE = "alertMessage"
const val EDIT_ALERT_BUTTON = "editAlertButton"
const val RESOLVE_ALERT_BUTTON = "resolveAlertButton"
const val ACCEPT_ALERT_BUTTON = "acceptAlertButton"
}

/** Constants for tagging UI components in the BottomNavigationMenu. */
Expand Down
70 changes: 30 additions & 40 deletions app/src/main/java/com/android/periodpals/ui/alert/AlertLists.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -73,23 +74,19 @@ import com.android.periodpals.resources.ComponentColor.getPrimaryCardColors
import com.android.periodpals.resources.ComponentColor.getTertiaryCardColors
import com.android.periodpals.services.GPSServiceImpl
import com.android.periodpals.services.NetworkChangeListener
import com.android.periodpals.ui.components.FILTERS_NO_PREFERENCE_TEXT
import com.android.periodpals.ui.components.FilterDialog
import com.android.periodpals.ui.components.FilterFab
import com.android.periodpals.ui.components.formatAlertTime
import com.android.periodpals.ui.navigation.BottomNavigationMenu
import com.android.periodpals.ui.navigation.LIST_TOP_LEVEL_DESTINATION
import com.android.periodpals.ui.navigation.NavigationActions
import com.android.periodpals.ui.navigation.Screen
import com.android.periodpals.ui.navigation.TopAppBar
import com.android.periodpals.ui.theme.dimens
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

private val SELECTED_TAB_DEFAULT = AlertListsTab.MY_ALERTS

private val INPUT_DATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME
private val OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm")

private const val TAG = "AlertListsScreen"

private const val DEFAULT_RADIUS = 100.0
Expand All @@ -106,7 +103,7 @@ private enum class AlertListsTab {
*
* @param alertViewModel The view model for managing alert data.
* @param authenticationViewModel The view model for managing authentication data.
* @param navigationActions The navigation actions for handling navigation events.
* @param locationViewModel The view model for managing the location data.
* @param gpsService The GPS service that provides the device's geographical coordinates.
* @param navigationActions The navigation actions for handling navigation events.
*/
Expand All @@ -126,7 +123,7 @@ fun AlertListsScreen(
var showFilterDialog by remember { mutableStateOf(false) }
var isFilterApplied by remember { mutableStateOf(false) }
var selectedLocation by remember { mutableStateOf<Location?>(null) }
var radiusInMeters by remember { mutableDoubleStateOf(100.0) }
var radiusInMeters by remember { mutableDoubleStateOf(DEFAULT_RADIUS) }
var productFilter by remember { mutableStateOf<Product?>(Product.NO_PREFERENCE) }
var urgencyFilter by remember { mutableStateOf<Urgency?>(null) }

Expand All @@ -142,12 +139,15 @@ fun AlertListsScreen(

val uid by remember { mutableStateOf(authenticationViewModel.authUserData.value!!.uid) }
alertViewModel.setUserID(uid)
alertViewModel.fetchAlerts(
onSuccess = {
alertViewModel.alerts.value
alertViewModel.removeFilters()
},
onFailure = { e -> Log.d(TAG, "Error fetching alerts: $e") })

LaunchedEffect(Unit) {
alertViewModel.fetchAlerts(
onSuccess = {
alertViewModel.alerts.value
alertViewModel.removeFilters()
},
onFailure = { e -> Log.d(TAG, "Error fetching alerts: $e") })
}

val myAlertsList = alertViewModel.myAlerts.value
var palsAlertsList by remember { mutableStateOf(alertViewModel.palAlerts) }
Expand Down Expand Up @@ -216,27 +216,32 @@ fun AlertListsScreen(
context = context,
currentRadius = radiusInMeters,
location = selectedLocation,
product = productToPeriodPalsIcon(productFilter!!).textId,
product =
productFilter?.let { productToPeriodPalsIcon(it).textId }
?: FILTERS_NO_PREFERENCE_TEXT,
urgency =
if (urgencyFilter == null) context.getString(R.string.alert_lists_filter_default)
else urgencyToPeriodPalsIcon(urgencyFilter!!).textId,
urgencyFilter?.let { urgencyToPeriodPalsIcon(it).textId }
?: FILTERS_NO_PREFERENCE_TEXT,
onDismiss = { showFilterDialog = false },
onLocationSelected = { selectedLocation = it },
onSave = { radius, product, urgency ->
radiusInMeters = radius
productFilter = stringToProduct(product)
urgencyFilter = stringToUrgency(urgency)
isFilterApplied = true
if (selectedLocation != null) {
isFilterApplied =
(radius != 100.0) ||
(productFilter != Product.NO_PREFERENCE) ||
(urgencyFilter != null)

selectedLocation?.let {
alertViewModel.fetchAlertsWithinRadius(
selectedLocation!!,
radiusInMeters,
location = it,
radius = radiusInMeters,
onSuccess = {
palsAlertsList = alertViewModel.palAlerts
Log.d(TAG, "Alerts within radius: $palsAlertsList")
Log.d(TAG, "Successfully fetched alerts within radius: $radiusInMeters")
},
onFailure = { e -> Log.d(TAG, "Error fetching alerts within radius: $e") })
}
onFailure = { e -> Log.e(TAG, "Error fetching alerts within radius", e) })
} ?: Log.d(TAG, "Selected location is null")

// if a product filter was selected, show only alerts with said product marked as needed
// (or alerts with no product preference)
Expand Down Expand Up @@ -487,21 +492,6 @@ private fun AlertProfilePicture(idTestTag: String) {
)
}

/**
* Formats the alert creation time to a readable string.
*
* @param createdAt The creation time of the alert in ISO_OFFSET_DATE_TIME format.
* @return A formatted time string or "Invalid Time" if the input is invalid.
*/
private fun formatAlertTime(createdAt: String?): String {
return try {
val dateTime = OffsetDateTime.parse(createdAt, INPUT_DATE_FORMATTER)
dateTime.format(OUTPUT_TIME_FORMATTER)
} catch (e: DateTimeParseException) {
throw DateTimeParseException("Invalid or null input for alert creation time", createdAt, 0)
}
}

/**
* Composable function that displays the time and location of an alert.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ import com.android.periodpals.resources.ComponentColor.getMenuTextFieldColors
import com.android.periodpals.resources.ComponentColor.getOutlinedTextFieldColors
import com.android.periodpals.services.GPSServiceImpl
import com.android.periodpals.ui.theme.dimens
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import kotlin.math.roundToInt

private const val PRODUCT_DROPDOWN_LABEL = "Product Needed"
Expand All @@ -88,6 +91,13 @@ private const val MIN_RADIUS = 100
private const val MAX_RADIUS = 1000
private const val KILOMETERS_IN_METERS = 1000

private val INPUT_DATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME
private val OUTPUT_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm")

private const val LOCATION_TEXT_LEN_LIMIT = 30

const val FILTERS_NO_PREFERENCE_TEXT = "No Preference"

/**
* Composable function for displaying a product selection dropdown menu.
*
Expand Down Expand Up @@ -593,4 +603,38 @@ fun SliderMenu(
* @param s The string to be capitalized.
* @return The capitalized string.
*/

/**
* Formats the alert creation time to a readable string.
*
* @param createdAt The creation time of the alert in ISO_OFFSET_DATE_TIME format.
* @return A formatted time string or "Invalid Time" if the input is invalid.
*/
fun formatAlertTime(createdAt: String?): String {
return try {
val dateTime = OffsetDateTime.parse(createdAt, INPUT_DATE_FORMATTER)
dateTime.format(OUTPUT_TIME_FORMATTER)
} catch (e: DateTimeParseException) {
throw DateTimeParseException("Invalid or null input for alert creation time", createdAt, 0)
}
}

/**
* Capitalizes the first letter of the string.
*
* @param s String whose first letter will be capitilized.
* @return Capitalized string.
*/
fun capitalized(s: String): String = s.lowercase().replaceFirstChar { it.uppercase() }

/**
* Trims the location name to only have [LOCATION_TEXT_LEN_LIMIT]. After it, "..." is added.
*
* @param locationText Name text to be trimmed.
* @return Trimmed text.
*/
fun trimLocationText(locationText: String): String {
if (locationText.length >= LOCATION_TEXT_LEN_LIMIT)
return locationText.take(LOCATION_TEXT_LEN_LIMIT) + "..."
return locationText
}
Loading

0 comments on commit 4ec5870

Please sign in to comment.