diff --git a/app/src/main/java/com/example/prueba/MainActivity.kt b/app/src/main/java/com/example/prueba/MainActivity.kt index 9ded489..8fef14b 100644 --- a/app/src/main/java/com/example/prueba/MainActivity.kt +++ b/app/src/main/java/com/example/prueba/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -23,18 +24,20 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color 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 com.example.prueba.data.GasStationInfo import com.example.prueba.data.GasStationRepository +import com.example.prueba.data.PriceCategory import com.example.prueba.ui.theme.PruebaTheme import com.example.prueba.utils.FavoriteStorage import com.example.prueba.utils.FuelRecord import com.example.prueba.utils.FuelStorage import com.example.prueba.utils.LocationStorage import com.google.android.gms.location.* -import com.google.android.gms.tasks.CancellationTokenSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -68,6 +71,8 @@ fun GasStationApp(repository: GasStationRepository) { var isLoading by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) } var selectedTab by remember { mutableIntStateOf(0) } + var searchQuery by remember { mutableStateOf("") } + var isSearchActive by remember { mutableStateOf(false) } var hasPermission by remember { mutableStateOf( @@ -104,17 +109,21 @@ fun GasStationApp(repository: GasStationRepository) { } } - LaunchedEffect(hasPermission) { + LaunchedEffect(hasPermission, searchQuery, selectedTab) { if (hasPermission) { val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) - LocationStorage.getLastLocation(context)?.let { lastLoc -> - scope.launch { - updateAddress(lastLoc) - stations = repository.getCheapestAndNearest(context, lastLoc) - } + val loc = lastLocation ?: LocationStorage.getLastLocation(context) + + loc?.let { + stations = repository.getCheapestAndNearest( + context = context, + userLocation = it, + municipalityQuery = if (selectedTab == 0) searchQuery.ifBlank { null } else null, + onlyFavorites = (selectedTab == 1) + ) } - isLoading = true + if (selectedTab != 2) isLoading = true val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000) .setMinUpdateDistanceMeters(500f) .build() @@ -124,9 +133,13 @@ fun GasStationApp(repository: GasStationRepository) { val location = result.lastLocation ?: return lastLocation = location LocationStorage.saveLocation(context, location) - scope.launch { - updateAddress(location) - stations = repository.getCheapestAndNearest(context, location) + if (searchQuery.isBlank()) { + scope.launch { + updateAddress(location) + stations = repository.getCheapestAndNearest(context, location) + isLoading = false + } + } else { isLoading = false } } @@ -145,25 +158,56 @@ fun GasStationApp(repository: GasStationRepository) { Scaffold( modifier = Modifier.fillMaxSize(), topBar = { - CenterAlignedTopAppBar( - title = { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Gasolineras Baratas", style = MaterialTheme.typography.titleSmall) - Text( - text = currentAddress, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - maxLines = 1 - ) + Column { + CenterAlignedTopAppBar( + title = { + if (isSearchActive) { + TextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { Text("Buscar municipio...") }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + singleLine = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent + ), + trailingIcon = { + IconButton(onClick = { + searchQuery = "" + isSearchActive = false + }) { + Icon(Icons.Default.Close, contentDescription = "Cerrar") + } + } + ) + } else { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("Gasolineras Diésel", style = MaterialTheme.typography.titleSmall) + Text( + text = currentAddress, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1 + ) + } + } + }, + actions = { + if (!isSearchActive && selectedTab != 2) { + IconButton(onClick = { isSearchActive = true }) { + Icon(Icons.Default.Search, contentDescription = "Buscar") + } + } } - } - ) + ) + } }, bottomBar = { NavigationBar { NavigationBarItem( icon = { Icon(Icons.Default.Place, "Gasolineras") }, - label = { Text("Gasolineras") }, + label = { Text("Diésel") }, selected = selectedTab == 0, onClick = { selectedTab = 0 } ) @@ -203,7 +247,7 @@ fun GasStationApp(repository: GasStationRepository) { lastLocation?.let { loc -> isRefreshing = true scope.launch { - stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true) + stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null) isRefreshing = false } } @@ -223,22 +267,23 @@ fun GasStationApp(repository: GasStationRepository) { repository = repository, lastLocation = lastLocation, onLogClick = { showFuelDialog = it }, - onUpdateStations = { stations = it } + onUpdateStations = { stations = it }, + searchQuery = searchQuery ) } } else { - val favoriteStations = stations.filter { it.isFavorite } - if (favoriteStations.isEmpty()) { + if (stations.isEmpty()) { Text("No tienes gasolineras favoritas marcadas", modifier = Modifier.align(Alignment.Center)) } else { GasStationList( - stations = favoriteStations, + stations = stations, context = context, scope = scope, repository = repository, lastLocation = lastLocation, onLogClick = { showFuelDialog = it }, - onUpdateStations = { stations = it } + onUpdateStations = { stations = it }, + searchQuery = "" ) } } @@ -262,7 +307,6 @@ fun GasStationApp(repository: GasStationRepository) { showFuelDialog?.let { station -> FuelLogDialog( stationName = station.name, - pricePerLiter = station.price, onDismiss = { showFuelDialog = null }, onConfirm = { liters, total -> val record = FuelRecord( @@ -284,7 +328,6 @@ fun GasStationApp(repository: GasStationRepository) { editRecordDialog?.let { record -> FuelLogDialog( stationName = record.stationName, - pricePerLiter = record.pricePerLiter, initialLiters = record.liters.toString(), initialTotal = record.totalPrice.toString(), onDismiss = { editRecordDialog = null }, @@ -311,7 +354,8 @@ fun GasStationList( repository: GasStationRepository, lastLocation: android.location.Location?, onLogClick: (GasStationInfo) -> Unit, - onUpdateStations: (List) -> Unit + onUpdateStations: (List) -> Unit, + searchQuery: String ) { LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) { items(stations) { station -> @@ -325,9 +369,10 @@ fun GasStationList( onLogClick = { onLogClick(station) }, onFavoriteClick = { FavoriteStorage.toggleFavorite(context, station.id) - lastLocation?.let { loc -> + val loc = lastLocation ?: LocationStorage.getLastLocation(context) + loc?.let { scope.launch { - onUpdateStations(repository.getCheapestAndNearest(context, loc)) + onUpdateStations(repository.getCheapestAndNearest(context, it, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null)) } } } @@ -386,7 +431,6 @@ fun FuelHistoryScreen(records: List, onDelete: (FuelRecord) -> Unit, @Composable fun FuelLogDialog( stationName: String, - pricePerLiter: Double, initialLiters: String = "", initialTotal: String = "", onDismiss: () -> Unit, @@ -431,6 +475,21 @@ fun FuelLogDialog( @Composable fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () -> Unit, onFavoriteClick: () -> Unit) { + val priceColor = when (station.priceCategory) { + PriceCategory.CHEAP -> Color(0xFF2E7D32) + PriceCategory.NORMAL -> MaterialTheme.colorScheme.primary + PriceCategory.EXPENSIVE -> Color(0xFFC62828) + } + + val brandColor = when { + station.name.contains("REPSOL", ignoreCase = true) -> Color(0xFFE30613) + station.name.contains("CEPSA", ignoreCase = true) -> Color(0xFFEC0000) + station.name.contains("BP", ignoreCase = true) -> Color(0xFF00A94F) + station.name.contains("GALP", ignoreCase = true) -> Color(0xFFFF6B00) + station.name.contains("SHELL", ignoreCase = true) -> Color(0xFFFFD500) + else -> MaterialTheme.colorScheme.surfaceVariant + } + Card( modifier = Modifier .fillMaxWidth() @@ -446,6 +505,11 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () ) { Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { + Surface( + color = brandColor, + modifier = Modifier.size(12.dp).padding(end = 8.dp), + shape = androidx.compose.foundation.shape.CircleShape + ) {} Text( text = "${station.name} - ${station.municipality}", style = MaterialTheme.typography.titleMedium, @@ -471,9 +535,9 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () shape = MaterialTheme.shapes.medium ) { Text( - text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km", + text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km (recta)", modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer ) } @@ -482,14 +546,24 @@ fun GasStationItem(station: GasStationInfo, onClick: () -> Unit, onLogClick: () Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.Bottom ) { - Text( - text = "${station.price} €/L", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.primary, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold - ) + Column { + Text( + text = "Diésel A: ${station.price} €/L", + style = MaterialTheme.typography.titleLarge, + color = priceColor, + fontWeight = FontWeight.Bold + ) + station.pricePremium?.let { premium -> + Text( + text = "Premium: $premium €/L", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Medium + ) + } + } Button( onClick = onLogClick, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), diff --git a/app/src/main/java/com/example/prueba/MainScreen.kt b/app/src/main/java/com/example/prueba/MainScreen.kt index f755684..06bb3b0 100644 --- a/app/src/main/java/com/example/prueba/MainScreen.kt +++ b/app/src/main/java/com/example/prueba/MainScreen.kt @@ -147,68 +147,74 @@ class MainScreen(carContext: CarContext) : Screen(carContext) { for (station in gasStations) { val distanceStr = String.format(Locale.getDefault(), "%.1f km", station.distance / 1000) - listBuilder.addItem( - Row.Builder() - .setTitle("${station.name} - ${station.municipality}") - .addText("${station.price} €/L • $distanceStr") - .addText(station.address) - .setOnClickListener { - val pane = Pane.Builder() - .addRow(Row.Builder().setTitle("Precio").addText("${station.price} €/L").build()) - .addRow(Row.Builder().setTitle("Distancia").addText(distanceStr).build()) - .addAction( - Action.Builder() - .setTitle(if (station.isFavorite) "Quitar Favorito" else "Añadir Favorito") - .setOnClickListener { - FavoriteStorage.toggleFavorite(carContext, station.id) - screenManager.pop() - fetchLocationAndGasStations(forceRefresh = false) - } - .build() - ) - .addAction( - Action.Builder() - .setTitle("Ir ahora") - .setOnClickListener { - val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}") - val intent = Intent(CarContext.ACTION_NAVIGATE, uri) - carContext.startCarApp(intent) - } - .build() - ) - .addAction( - Action.Builder() - .setTitle("Registrar Repostaje") - .setOnClickListener { - // Guardar record simplificado desde el coche - val record = FuelRecord( - date = System.currentTimeMillis(), - stationName = station.name, - address = station.address, - municipality = station.municipality, - liters = 0.0, // Se editará en el móvil después - totalPrice = 0.0, - pricePerLiter = station.price - ) - FuelStorage.saveRecord(carContext, record) - screenManager.pop() - } - .build() - ) - .build() - - screenManager.push( - object : Screen(carContext) { - override fun onGetTemplate(): Template { - return PaneTemplate.Builder(pane) - .setHeader(Header.Builder().setTitle(station.name).setStartHeaderAction(Action.BACK).build()) - .build() - } - } - ) + + val rowBuilder = Row.Builder() + .setTitle("${station.name} - ${station.municipality}") + .addText("A: ${station.price} €/L" + (station.pricePremium?.let { " • P: $it €/L" } ?: "")) + .addText("$distanceStr (recta) • ${station.address}") + .setOnClickListener { + val paneBuilder = Pane.Builder() + .addRow(Row.Builder().setTitle("Diésel A").addText("${station.price} €/L").build()) + + station.pricePremium?.let { + paneBuilder.addRow(Row.Builder().setTitle("Diésel Premium").addText("$it €/L").build()) } - .build() - ) + + val pane = paneBuilder + .addRow(Row.Builder().setTitle("Ubicación").addText("${station.municipality}\n${station.address}").build()) + .addRow(Row.Builder().setTitle("Distancia").addText("$distanceStr (en línea recta)").build()) + .addAction( + Action.Builder() + .setTitle(if (station.isFavorite) "Quitar Favorito" else "Añadir Favorito") + .setOnClickListener { + FavoriteStorage.toggleFavorite(carContext, station.id) + screenManager.pop() + fetchLocationAndGasStations(forceRefresh = false) + } + .build() + ) + .addAction( + Action.Builder() + .setTitle("Ir ahora") + .setOnClickListener { + val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}") + val intent = Intent(CarContext.ACTION_NAVIGATE, uri) + carContext.startCarApp(intent) + } + .build() + ) + .addAction( + Action.Builder() + .setTitle("Registrar") + .setOnClickListener { + val record = FuelRecord( + date = System.currentTimeMillis(), + stationName = station.name, + address = station.address, + municipality = station.municipality, + liters = 0.0, + totalPrice = 0.0, + pricePerLiter = station.price + ) + FuelStorage.saveRecord(carContext, record) + screenManager.pop() + } + .build() + ) + .build() + + screenManager.push( + object : Screen(carContext) { + override fun onGetTemplate(): Template { + return PaneTemplate.Builder(pane) + .setHeader(Header.Builder().setTitle(station.name).setStartHeaderAction(Action.BACK).build()) + .build() + } + } + ) + } + + listBuilder.addItem(rowBuilder.build()) } return ListTemplate.Builder() diff --git a/app/src/main/java/com/example/prueba/data/GasStationRepository.kt b/app/src/main/java/com/example/prueba/data/GasStationRepository.kt index 451d41a..239be03 100644 --- a/app/src/main/java/com/example/prueba/data/GasStationRepository.kt +++ b/app/src/main/java/com/example/prueba/data/GasStationRepository.kt @@ -15,7 +15,13 @@ class GasStationRepository { .build() .create(GasolineraApi::class.java) - suspend fun getCheapestAndNearest(context: Context, userLocation: Location, forceRefresh: Boolean = false): List { + suspend fun getCheapestAndNearest( + context: Context, + userLocation: Location, + forceRefresh: Boolean = false, + municipalityQuery: String? = null, + onlyFavorites: Boolean = false + ): List { return try { if (cachedResponse == null || forceRefresh) { cachedResponse = api.getGasolineras() @@ -23,47 +29,76 @@ class GasStationRepository { val favorites = FavoriteStorage.getFavoriteIds(context) - cachedResponse?.listaEESS?.mapNotNull { + val allStations = cachedResponse?.listaEESS?.mapNotNull { val lat = it.latitud.replace(",", ".").toDoubleOrNull() val lon = it.longitud.replace(",", ".").toDoubleOrNull() - val price = it.precioDiesel.replace(",", ".").toDoubleOrNull() ?: it.precioGasolina95.replace(",", ".").toDoubleOrNull() - - if (lat != null && lon != null && price != null) { - val stationLoc = Location("").apply { - latitude = lat - longitude = lon - } - val distance = userLocation.distanceTo(stationLoc) - GasStationInfo( - id = it.id, - name = it.rotulo, - address = it.direccion, - municipality = it.municipio, - price = price, - distance = distance, - latitude = lat, - longitude = lon, - isFavorite = favorites.contains(it.id) - ) + + if (lat != null && lon != null) { + val dieselPrice = it.precioDiesel.replace(",", ".").toDoubleOrNull() + val premiumPrice = it.precioDieselPremium.replace(",", ".").toDoubleOrNull() + + if (dieselPrice != null || premiumPrice != null) { + val stationLoc = Location("").apply { + latitude = lat + longitude = lon + } + val distance = userLocation.distanceTo(stationLoc) + GasStationInfo( + id = it.id, + name = it.rotulo, + address = it.direccion, + municipality = it.municipio, + price = dieselPrice ?: 0.0, + pricePremium = premiumPrice, + distance = distance, + latitude = lat, + longitude = lon, + isFavorite = favorites.contains(it.id) + ) + } else null } else null - }?.sortedBy { it.distance } - ?.take(50) - ?.sortedWith(compareByDescending { it.isFavorite }.thenBy { it.price }) - ?.take(10) ?: emptyList() + } ?: emptyList() + + val filteredStations = when { + onlyFavorites -> allStations.filter { it.isFavorite } + .sortedBy { it.distance } + !municipalityQuery.isNullOrBlank() -> allStations.filter { it.municipality.contains(municipalityQuery, ignoreCase = true) } + .sortedBy { it.distance } + else -> allStations.sortedBy { it.distance }.take(50) + } + + if (filteredStations.isEmpty()) return emptyList() + + val minPrice = filteredStations.minOf { it.price } + val avgPrice = filteredStations.map { it.price }.average() + + filteredStations.map { station -> + val category = when { + station.price <= minPrice * 1.01 -> PriceCategory.CHEAP + station.price <= avgPrice -> PriceCategory.NORMAL + else -> PriceCategory.EXPENSIVE + } + station.copy(priceCategory = category) + }.sortedWith(compareByDescending { it.isFavorite }.thenBy { it.price }) + .take(if (onlyFavorites) 50 else 15) } catch (e: Exception) { emptyList() } } } +enum class PriceCategory { CHEAP, NORMAL, EXPENSIVE } + data class GasStationInfo( val id: String, val name: String, val address: String, val municipality: String, val price: Double, + val pricePremium: Double? = null, val distance: Float, val latitude: Double, val longitude: Double, - val isFavorite: Boolean = false + val isFavorite: Boolean = false, + val priceCategory: PriceCategory = PriceCategory.NORMAL ) diff --git a/app/src/main/java/com/example/prueba/data/GasolineraApi.kt b/app/src/main/java/com/example/prueba/data/GasolineraApi.kt index 4f2d699..f4e1fd3 100644 --- a/app/src/main/java/com/example/prueba/data/GasolineraApi.kt +++ b/app/src/main/java/com/example/prueba/data/GasolineraApi.kt @@ -19,6 +19,8 @@ data class Gasolinera( val municipio: String, @SerializedName("Precio Gasoleo A") val precioDiesel: String, + @SerializedName("Precio Gasoleo Premium") + val precioDieselPremium: String, @SerializedName("Precio Gasolina 95 E5") val precioGasolina95: String, @SerializedName("Latitud")