diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt index ca86a45a..73c4fb1b 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.BatteryChargingFull import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.ElectricBolt @@ -39,12 +40,15 @@ import androidx.compose.material.icons.filled.Schedule import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -153,6 +157,10 @@ fun ChargesScreen( initialScrollOffset = uiState.scrollOffset, palette = palette, onDateFilterSelected = { viewModel.setDateFilter(it) }, + availableLocations = uiState.availableLocations, + selectedLocations = uiState.selectedLocations, + onLocationFilterToggled = { viewModel.setLocationFilter(it) }, + onLocationFilterCleared = { viewModel.clearLocationFilter() }, onChargeTypeFilterSelected = { viewModel.setChargeTypeFilter(it) }, onChargeClick = { chargeId, scrollIndex, scrollOffset -> viewModel.saveScrollPosition(scrollIndex, scrollOffset) @@ -176,6 +184,10 @@ private fun ChargesContent( teslamateBaseUrl: String, selectedDateFilter: DateFilter, selectedChargeTypeFilter: ChargeTypeFilter, + availableLocations: List, + selectedLocations: Set, + onLocationFilterToggled: (String) -> Unit, + onLocationFilterCleared: () -> Unit, initialScrollPosition: Int, initialScrollOffset: Int, palette: CarColorPalette, @@ -204,11 +216,25 @@ private fun ChargesContent( } item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { ChargeTypeFilterChips( selectedFilter = selectedChargeTypeFilter, - palette = palette, - onFilterSelected = onChargeTypeFilterSelected - ) + onFilterSelected = onChargeTypeFilterSelected, + modifier = Modifier.weight(1f), + palette = palette + ) + LocationFilterDropdown( + availableLocations = availableLocations, + selectedLocations = selectedLocations, + onLocationToggled = onLocationFilterToggled, + onClearAll = onLocationFilterCleared, + palette = palette + ) + } } item { @@ -316,9 +342,11 @@ private fun DateFilterChips( private fun ChargeTypeFilterChips( selectedFilter: ChargeTypeFilter, palette: CarColorPalette, + modifier: Modifier = Modifier, onFilterSelected: (ChargeTypeFilter) -> Unit ) { LazyRow( + modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(ChargeTypeFilter.entries.toList()) { filter -> @@ -877,39 +905,70 @@ private fun ChargesChartPage( } } -/* @Composable -private fun ChartLegend( - palette: CarColorPalette +private fun LocationFilterDropdown( + availableLocations: List, + selectedLocations: Set, + palette: CarColorPalette, + onLocationToggled: (String) -> Unit, + onClearAll: () -> Unit ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - LegendItem(color = Color(0xFF4CAF50) , label = stringResource(R.string.charging_ac)) - Spacer(modifier = Modifier.width(24.dp)) - LegendItem(color = Color(0xFFFF9800), label = stringResource(R.string.charging_dc)) - } -} - -@Composable -private fun LegendItem(color: Color, label: String) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(12.dp) - .clip(RoundedCornerShape(2.dp)) - .background(color) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + var expanded by remember { mutableStateOf(false) } + var searchText by remember { mutableStateOf("") } + val hasSelection = selectedLocations.isNotEmpty() + + Box { + FilterChip( + selected = hasSelection, + onClick = { expanded = true }, + label = { + Text( + if (hasSelection && selectedLocations.size == 1) + selectedLocations.first() + else if (hasSelection) + "${selectedLocations.size} ubicaciones" + else + stringResource(R.string.filter_location) + ) + }, + leadingIcon = { Icon(Icons.Default.LocationOn, null, Modifier.size(16.dp)) }, + trailingIcon = if (hasSelection) { + { + Icon( + Icons.Default.Clear, + contentDescription = null, + modifier = Modifier.size(16.dp).clickable { onClearAll() } + ) + } + } else null ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false; searchText = "" } + ) { + OutlinedTextField( + value = searchText, + onValueChange = { searchText = it }, + placeholder = { Text(stringResource(R.string.search)) }, + modifier = Modifier.padding(8.dp).fillMaxWidth(), + singleLine = true + ) + val filtered = availableLocations.filter { + it.contains(searchText, ignoreCase = true) + } + filtered.forEach { location -> + val isSelected = location in selectedLocations + DropdownMenuItem( + text = { Text(location) }, + leadingIcon = { + if (isSelected) + Icon(Icons.Default.Check, null, tint = palette.accent) + else + Spacer(Modifier.size(24.dp)) + }, + onClick = { onLocationToggled(location) } + ) + } + } } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt index 1827ce2d..b82af596 100644 --- a/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt +++ b/app/src/main/java/com/matedroid/ui/screens/charges/ChargesViewModel.kt @@ -45,6 +45,8 @@ enum class ChargeTypeFilter(val label: String) { DC("DC") } +data class LocationFilter(val name: String) // null name = All locations + data class ChargeChartData( val label: String, val count: Int, @@ -64,6 +66,8 @@ data class ChargesUiState( val isRefreshing: Boolean = false, val charges: List = emptyList(), val dcChargeIds: Set = emptySet(), + val availableLocations: List = emptyList(), + val selectedLocations: Set = emptySet(), // null = All locations val processedChargeIds: Set = emptySet(), // Charges that have aggregate data val chartData: List = emptyList(), val chartGranularity: ChartGranularity = ChartGranularity.MONTHLY, @@ -164,6 +168,18 @@ class ChargesViewModel @Inject constructor( applyFiltersAndUpdateState() } + fun setLocationFilter(location: String) { + val current = _uiState.value.selectedLocations + val updated = if (location in current) current - location else current + location + _uiState.update { it.copy(selectedLocations = updated) } + applyFiltersAndUpdateState() + } + + fun clearLocationFilter() { + _uiState.update { it.copy(selectedLocations = emptySet()) } + applyFiltersAndUpdateState() + } + fun clearError() { _uiState.update { it.copy(error = null) } } @@ -259,15 +275,28 @@ class ChargesViewModel @Inject constructor( ChargeTypeFilter.AC -> allCharges.filter { it.chargeId !in dcChargeIds } } + // Extract unique locations from the complete set + val locations = allCharges.mapNotNull { it.address }.distinct().sorted() + // Apply location filter to displayCharges + val locationFilter = state.selectedLocations + val displayChargesFiltered = if (locationFilter.isNotEmpty()) + displayCharges.filter { it.address in locationFilter } + else displayCharges + // Apply location filter to Stats + val chargesForStatsFiltered = if (locationFilter.isNotEmpty()) + chargesForStats.filter { it.address in locationFilter } + else chargesForStats + // Calculate summary and chart data from filtered charges - val summary = calculateSummary(chargesForStats) - val chartData = calculateChartData(chargesForStats, granularity, state.startDate) + val summary = calculateSummary(chargesForStatsFiltered) + val chartData = calculateChartData(chargesForStatsFiltered, granularity, state.startDate) _uiState.update { it.copy( isLoading = false, isRefreshing = false, - charges = displayCharges, + charges = displayChargesFiltered, + availableLocations = locations, summary = summary, chartData = chartData ) diff --git a/app/src/main/java/com/matedroid/ui/theme/Color.kt b/app/src/main/java/com/matedroid/ui/theme/Color.kt index f8485893..aaf1620b 100644 --- a/app/src/main/java/com/matedroid/ui/theme/Color.kt +++ b/app/src/main/java/com/matedroid/ui/theme/Color.kt @@ -30,6 +30,7 @@ val SurfaceVariantLight = Color(0xFFE8EAEC) val OnSurfaceVariantLight = Color(0xFF44474A) val OutlineLight = Color(0xFF74777A) val OutlineVariantLight = Color(0xFFC4C6C8) +val SurfaceContainerLight = Color(0xFFF0F1F3) val SurfaceContainerHighLight = Color(0xFFECEDEF) val SurfaceContainerHighestLight = Color(0xFFE6E7E9) val ErrorLight = StatusError @@ -54,6 +55,7 @@ val SurfaceVariantDark = Color(0xFF44474A) val OnSurfaceVariantDark = Color(0xFFCACCCE) val OutlineDark = Color(0xFF8E9194) val OutlineVariantDark = Color(0xFF44474A) +val SurfaceContainerDark = Color(0xFF242629) val SurfaceContainerHighDark = Color(0xFF2B2D31) val SurfaceContainerHighestDark = Color(0xFF35373B) val ErrorDark = Color(0xFFFFB4AB) diff --git a/app/src/main/java/com/matedroid/ui/theme/Theme.kt b/app/src/main/java/com/matedroid/ui/theme/Theme.kt index a85e4bca..b9d1b628 100644 --- a/app/src/main/java/com/matedroid/ui/theme/Theme.kt +++ b/app/src/main/java/com/matedroid/ui/theme/Theme.kt @@ -25,6 +25,7 @@ private val DarkColorScheme = darkColorScheme( onSurfaceVariant = OnSurfaceVariantDark, outline = OutlineDark, outlineVariant = OutlineVariantDark, + surfaceContainer = SurfaceContainerDark, surfaceContainerHigh = SurfaceContainerHighDark, surfaceContainerHighest = SurfaceContainerHighestDark, error = ErrorDark, @@ -50,6 +51,7 @@ private val LightColorScheme = lightColorScheme( onSurfaceVariant = OnSurfaceVariantLight, outline = OutlineLight, outlineVariant = OutlineVariantLight, + surfaceContainer = SurfaceContainerLight, surfaceContainerHigh = SurfaceContainerHighLight, surfaceContainerHighest = SurfaceContainerHighestLight, error = ErrorLight, diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index fcfbdbea..801b103c 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -359,6 +359,10 @@ Historial de càrregues + + Ubicació + Cerca + No s\'han trobat càrregues per al període seleccionat diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 92aaa014..e73bd16b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -359,6 +359,10 @@ Historial de cargas + + Ubicación + Buscar + No se encontraron cargas para el período seleccionado diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f6cbb2f0..c2b96439 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -359,6 +359,10 @@ Cronologia ricariche + + Posizione + Ricerca + Nessuna ricarica trovata per il periodo selezionato diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d814891c..0bee6620 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -359,6 +359,10 @@ Charge History + + Location + Search + No charges found for selected period