package com.example.prueba import android.Manifest import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.location.Geocoder import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity 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 import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale class MainActivity : ComponentActivity() { private val repository = GasStationRepository() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PruebaTheme { GasStationApp(repository) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun GasStationApp(repository: GasStationRepository) { val context = LocalContext.current val scope = rememberCoroutineScope() var stations by remember { mutableStateOf>(emptyList()) } var lastLocation by remember { mutableStateOf(null) } var fuelRecords by remember { mutableStateOf(FuelStorage.getRecords(context)) } var showFuelDialog by remember { mutableStateOf(null) } var editRecordDialog by remember { mutableStateOf(null) } var currentAddress by remember { mutableStateOf("Buscando ubicación...") } 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( ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) } val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { isGranted -> hasPermission = isGranted } suspend fun updateAddress(location: android.location.Location) { withContext(Dispatchers.IO) { try { val geocoder = Geocoder(context, Locale.getDefault()) val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1) if (!addresses.isNullOrEmpty()) { val address = addresses[0] val street = address.thoroughfare val number = address.subThoroughfare currentAddress = if (street != null && number != null) { "$street, $number" } else { street ?: address.getAddressLine(0) ?: "Ubicación desconocida" } } } catch (ignore: Exception) {} } } LaunchedEffect(hasPermission, searchQuery, selectedTab) { if (hasPermission) { val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) 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) ) } if (selectedTab != 2) isLoading = true val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000) .setMinUpdateDistanceMeters(500f) .build() val locationCallback = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { val location = result.lastLocation ?: return lastLocation = location LocationStorage.saveLocation(context, location) if (searchQuery.isBlank()) { scope.launch { updateAddress(location) stations = repository.getCheapestAndNearest(context, location) isLoading = false } } else { isLoading = false } } } try { if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, android.os.Looper.getMainLooper()) } } catch (e: Exception) { isLoading = false } } } Scaffold( modifier = Modifier.fillMaxSize(), topBar = { 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("Diésel") }, selected = selectedTab == 0, onClick = { selectedTab = 0 } ) NavigationBarItem( icon = { Icon(Icons.Default.Favorite, "Favoritas") }, label = { Text("Favoritas") }, selected = selectedTab == 1, onClick = { selectedTab = 1 } ) NavigationBarItem( icon = { Icon(Icons.Default.DateRange, "Historial") }, label = { Text("Historial") }, selected = selectedTab == 2, onClick = { selectedTab = 2 } ) } } ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) { if (!hasPermission) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text("Se necesita permiso de ubicación") Button(onClick = { launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }) { Text("Dar permiso") } } } else { when (selectedTab) { 0, 1 -> { PullToRefreshBox( isRefreshing = isRefreshing, onRefresh = { lastLocation?.let { loc -> isRefreshing = true scope.launch { stations = repository.getCheapestAndNearest(context, loc, forceRefresh = true, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null) isRefreshing = false } } }, modifier = Modifier.fillMaxSize() ) { if (selectedTab == 0) { if (isLoading && stations.isEmpty()) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else if (stations.isEmpty()) { Text("No se encontraron gasolineras", modifier = Modifier.align(Alignment.Center)) } else { GasStationList( stations = stations, context = context, scope = scope, repository = repository, lastLocation = lastLocation, onLogClick = { showFuelDialog = it }, onUpdateStations = { stations = it }, searchQuery = searchQuery ) } } else { if (stations.isEmpty()) { Text("No tienes gasolineras favoritas marcadas", modifier = Modifier.align(Alignment.Center)) } else { GasStationList( stations = stations, context = context, scope = scope, repository = repository, lastLocation = lastLocation, onLogClick = { showFuelDialog = it }, onUpdateStations = { stations = it }, searchQuery = "" ) } } } } 2 -> { FuelHistoryScreen( records = fuelRecords, onDelete = { record -> FuelStorage.deleteRecord(context, record) fuelRecords = FuelStorage.getRecords(context) }, onEdit = { record -> editRecordDialog = record } ) } } } showFuelDialog?.let { station -> FuelLogDialog( stationName = station.name, onDismiss = { showFuelDialog = null }, onConfirm = { liters, total -> val record = FuelRecord( date = System.currentTimeMillis(), stationName = station.name, address = station.address, municipality = station.municipality, liters = liters, totalPrice = total, pricePerLiter = station.price ) FuelStorage.saveRecord(context, record) fuelRecords = FuelStorage.getRecords(context) showFuelDialog = null } ) } editRecordDialog?.let { record -> FuelLogDialog( stationName = record.stationName, initialLiters = record.liters.toString(), initialTotal = record.totalPrice.toString(), onDismiss = { editRecordDialog = null }, onConfirm = { liters, total -> val newRecord = record.copy( liters = liters, totalPrice = total ) FuelStorage.updateRecord(context, record, newRecord) fuelRecords = FuelStorage.getRecords(context) editRecordDialog = null } ) } } } } @Composable fun GasStationList( stations: List, context: android.content.Context, scope: kotlinx.coroutines.CoroutineScope, repository: GasStationRepository, lastLocation: android.location.Location?, onLogClick: (GasStationInfo) -> Unit, onUpdateStations: (List) -> Unit, searchQuery: String ) { LazyColumn(contentPadding = PaddingValues(vertical = 8.dp)) { items(stations) { station -> GasStationItem( station = station, onClick = { val uri = Uri.parse("geo:${station.latitude},${station.longitude}?q=${station.latitude},${station.longitude}(${Uri.encode(station.name)})") val intent = Intent(Intent.ACTION_VIEW, uri) context.startActivity(intent) }, onLogClick = { onLogClick(station) }, onFavoriteClick = { FavoriteStorage.toggleFavorite(context, station.id) val loc = lastLocation ?: LocationStorage.getLastLocation(context) loc?.let { scope.launch { onUpdateStations(repository.getCheapestAndNearest(context, it, municipalityQuery = if(searchQuery.isNotBlank()) searchQuery else null)) } } } ) } } } @Composable fun FuelHistoryScreen(records: List, onDelete: (FuelRecord) -> Unit, onEdit: (FuelRecord) -> Unit) { if (records.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Aún no tienes repostajes registrados") } } else { LazyColumn(contentPadding = PaddingValues(16.dp)) { items(records) { record -> Card( modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Column(modifier = Modifier.weight(1f)) { Text(record.stationName, style = MaterialTheme.typography.titleMedium) Text("${record.municipality} - ${record.address}", style = MaterialTheme.typography.bodySmall) } Row { IconButton(onClick = { onEdit(record) }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Edit, contentDescription = "Editar", modifier = Modifier.size(18.dp)) } Spacer(modifier = Modifier.width(8.dp)) IconButton(onClick = { onDelete(record) }, modifier = Modifier.size(24.dp)) { Icon(Icons.Default.Delete, contentDescription = "Eliminar", modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.error) } } } Text( android.text.format.DateFormat.format("dd/MM/yyyy", record.date).toString(), style = MaterialTheme.typography.labelSmall ) Spacer(modifier = Modifier.height(4.dp)) Text("${record.liters} L x ${record.pricePerLiter} €/L", style = MaterialTheme.typography.bodyMedium) Text( "Total: ${record.totalPrice} €", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary ) } } } } } } @Composable fun FuelLogDialog( stationName: String, initialLiters: String = "", initialTotal: String = "", onDismiss: () -> Unit, onConfirm: (Double, Double) -> Unit ) { var liters by remember { mutableStateOf(initialLiters) } var total by remember { mutableStateOf(initialTotal) } AlertDialog( onDismissRequest = onDismiss, title = { Text("Registrar Repostaje") }, text = { Column { Text(stationName, style = MaterialTheme.typography.bodyLarge) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = liters, onValueChange = { liters = it }, label = { Text("Litros") }, modifier = Modifier.fillMaxWidth() ) OutlinedTextField( value = total, onValueChange = { total = it }, label = { Text("Precio Total (€)") }, modifier = Modifier.fillMaxWidth() ) } }, confirmButton = { Button(onClick = { val l = liters.toDoubleOrNull() ?: 0.0 val t = total.toDoubleOrNull() ?: 0.0 if (l > 0 && t > 0) onConfirm(l, t) }) { Text("Guardar") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Cancelar") } } ) } @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() .padding(horizontal = 16.dp, vertical = 6.dp) .clickable { onClick() }, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { 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, maxLines = 1, modifier = Modifier.weight(1f) ) IconButton(onClick = onFavoriteClick) { Icon( imageVector = if (station.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, contentDescription = "Favorito", tint = if (station.isFavorite) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline ) } } Text( text = station.address, style = MaterialTheme.typography.bodySmall, maxLines = 1 ) } Surface( color = MaterialTheme.colorScheme.secondaryContainer, shape = MaterialTheme.shapes.medium ) { Text( text = "${String.format(Locale.getDefault(), "%.1f", station.distance / 1000)} km (recta)", modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSecondaryContainer ) } } Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom ) { 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), shape = MaterialTheme.shapes.small ) { Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.width(4.dp)) Text("Registrar") } } } } }