diff --git a/app/src/main/java/com/example/snapandsolve/MainScreen.kt b/app/src/main/java/com/example/snapandsolve/MainScreen.kt index 544eafc..a1b6bdc 100644 --- a/app/src/main/java/com/example/snapandsolve/MainScreen.kt +++ b/app/src/main/java/com/example/snapandsolve/MainScreen.kt @@ -1,5 +1,6 @@ package com.example.snapandsolve +import DamageFilterDialog import MapViewModel import android.Manifest import android.R.attr.enabled @@ -37,6 +38,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import applyDamageFilter import com.arcgismaps.data.ArcGISFeature // Hier holen wir die ArcGIS Klassen import com.arcgismaps.mapping.ArcGISMap @@ -48,6 +50,7 @@ import com.example.snapandsolve.camera.AlbumViewModel import com.example.snapandsolve.camera.AlbumViewState import com.example.snapandsolve.camera.Intent import com.example.snapandsolve.ui.theme.* +import getActiveFilters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -183,9 +186,9 @@ fun ContentScreen( damageTypes = MapViewModel.DAMAGE_TYPES, // <-- Nutzt zentrale Liste currentFilters = mapViewModel.getActiveFilters(), onDismiss = { showFilterDialog = false }, - onApplyFilter = { selectedTypes -> + onApplyFilter = { selectedTypes, startDate, endDate -> coroutineScope.launch { - mapViewModel.applyDamageFilter(selectedTypes) + mapViewModel.applyDamageFilter(selectedTypes, startDate, endDate) } } ) diff --git a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt index 337e0c5..960d700 100644 --- a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt +++ b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt @@ -82,11 +82,32 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { damageTypeList += it.name } println("DEBUG: ServiceFeatureTable erfolgreich geladen") + + // ===== DEBUG: Alle verfügbaren Felder ausgeben ===== + println("DEBUG: Verfügbare Felder in ServiceFeatureTable:") + serviceFeatureTable.fields.forEach { field -> + println(" - ${field.name}") + } + println("DEBUG: Ende Feldliste") + }.onFailure { println("DEBUG: Fehler beim Laden der Tabelle: ${it.message}") } featureLayer = FeatureLayer.createWithFeatureTable(serviceFeatureTable) map.operationalLayers.add(featureLayer) + + // ===== DEBUG: Felder nach dem Hinzufügen zur Map ===== + featureLayer.load().onSuccess { + println("DEBUG: FeatureLayer erfolgreich geladen") + val table = featureLayer.featureTable + if (table != null) { + println("DEBUG: Verfügbare Felder im FeatureLayer:") + table.fields.forEach { field -> + println(" - ${field.name}") + } + println("DEBUG: Ende Feldliste FeatureLayer") + } + } } } @@ -365,7 +386,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) { viewModelScope.launch { // Determine if a user tapped on a feature. mapViewProxy.identify(featureLayer, screenCoordinate, 10.dp).onSuccess { - identifyResult -> + identifyResult -> // Get the identified feature. val identifiedFeature = identifyResult.geoElements.firstOrNull() as? ArcGISFeature identifiedFeature?.let { @@ -403,7 +424,7 @@ data class ReportDraft( val isValid: Boolean get() = beschreibung.isNotBlank() && - typ != "Schadenstyp wählen..." && - point != null && - photos.isNotEmpty() + typ != "Schadenstyp wählen..." && + point != null && + photos.isNotEmpty() } \ No newline at end of file diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt index 6556ccc..3b07d29 100644 --- a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt @@ -1,8 +1,5 @@ -package com.example.snapandsolve.ui.theme - - - import MapViewModel +import MapViewModel.Companion.DAMAGE_TYPES import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -17,25 +14,38 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.time.LocalDate +import java.time.format.DateTimeFormatter /** * Dialog für Schaden-Filter - * Ermöglicht das Filtern von Features nach Typ + * Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander) */ @Composable fun DamageFilterDialog( damageTypes: List, currentFilters: Set, onDismiss: () -> Unit, - onApplyFilter: (Set) -> Unit + onApplyFilter: (Set, LocalDate?, LocalDate?) -> Unit ) { - var selectedFilters by remember { mutableStateOf(currentFilters) } + // WICHTIG: Wenn keine Filter aktiv sind, alle Typen standardmäßig auswählen! + // Das ermöglicht Datumfilterung ohne Typ-Auswahl + var selectedFilters by remember { + mutableStateOf( + if (currentFilters.isEmpty()) damageTypes.toSet() else currentFilters + ) + } + var startDateString by remember { mutableStateOf("") } + var endDateString by remember { mutableStateOf("") } + var startDate by remember { mutableStateOf(null) } + var endDate by remember { mutableStateOf(null) } + var useDateFilter by remember { mutableStateOf(false) } Dialog(onDismissRequest = onDismiss) { Card( modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.7f), + .fillMaxWidth(0.9f) + .fillMaxHeight(0.85f), elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) ) { Column( @@ -62,19 +72,28 @@ fun DamageFilterDialog( Divider(modifier = Modifier.padding(vertical = 16.dp)) - // Info-Text - Text( - text = "Wähle die Schadenstypen aus, die angezeigt werden sollen:", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) - ) - - // Filter-Liste + // Scroll-Content Column( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) ) { + // ===== TYP-FILTER ===== + Text( + text = "Schadenstypen:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 12.dp) + ) + + // Info-Text + Text( + text = "Wähle die Schadenstypen aus, die angezeigt werden sollen:", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 12.dp) + ) + + // Filter-Liste damageTypes.forEach { type -> FilterCheckboxItem( label = type, @@ -88,17 +107,118 @@ fun DamageFilterDialog( } ) } + + Divider(modifier = Modifier.padding(vertical = 16.dp)) + + // ===== DATUMS-FILTER ===== + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = useDateFilter, + onCheckedChange = { useDateFilter = it } + ) + Text( + text = "Nach Datum filtern", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 8.dp) + ) + } + + // Datums-Eingabe (nur wenn aktiviert) + if (useDateFilter) { + // Von-Datum + Text( + text = "Von:", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 4.dp) + ) + OutlinedTextField( + value = startDateString, + onValueChange = { newValue -> + startDateString = newValue + startDate = try { + if (newValue.length == 10) { // dd.MM.yyyy format + LocalDate.parse( + newValue, + DateTimeFormatter.ofPattern("dd.MM.yyyy") + ) + } else { + null + } + } catch (e: Exception) { + null + } + }, + placeholder = { Text("dd.MM.yyyy") }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + singleLine = true + ) + + // Bis-Datum + Text( + text = "Bis:", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 4.dp) + ) + OutlinedTextField( + value = endDateString, + onValueChange = { newValue -> + endDateString = newValue + endDate = try { + if (newValue.length == 10) { // dd.MM.yyyy format + LocalDate.parse( + newValue, + DateTimeFormatter.ofPattern("dd.MM.yyyy") + ) + } else { + null + } + } catch (e: Exception) { + null + } + }, + placeholder = { Text("dd.MM.yyyy") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Text( + text = "Format: dd.MM.yyyy (z.B. 15.01.2024)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.padding(top = 8.dp, bottom = 12.dp) + ) + } } Divider(modifier = Modifier.padding(vertical = 16.dp)) // Info über aktive Filter - if (selectedFilters.isNotEmpty()) { + if (selectedFilters.isNotEmpty() || useDateFilter) { Text( - text = "${selectedFilters.size} von ${damageTypes.size} Typen ausgewählt", + text = buildString { + if (selectedFilters.size < damageTypes.size) { + append("${selectedFilters.size} von ${damageTypes.size} Typ(en)") + } else { + append("Alle Typen") + } + if (useDateFilter) { + if (selectedFilters.isNotEmpty()) append(" | ") + append("Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}") + } + }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(bottom = 8.dp) + modifier = Modifier.padding(bottom = 12.dp) ) } @@ -112,7 +232,7 @@ fun DamageFilterDialog( onClick = { selectedFilters = damageTypes.toSet() }, modifier = Modifier.weight(1f) ) { - Text("Alle") + Text("Alle Typen") } // Alle abwählen @@ -129,14 +249,18 @@ fun DamageFilterDialog( // Anwenden Button Button( onClick = { - onApplyFilter(selectedFilters) + val startDateToApply = if (useDateFilter) startDate else null + val endDateToApply = if (useDateFilter) endDate else null + onApplyFilter(selectedFilters, startDateToApply, endDateToApply) onDismiss() }, modifier = Modifier.fillMaxWidth() ) { Text( - if (selectedFilters.isEmpty()) "Alle anzeigen" - else "Filter anwenden" + when { + selectedFilters.isEmpty() && !useDateFilter -> "Alle anzeigen" + else -> "Filter anwenden" + } ) } } @@ -172,7 +296,6 @@ fun FilterCheckboxItem( modifier = Modifier.weight(1f) ) - Text( text = when (label) { "Straße" -> "🛣️" @@ -189,30 +312,69 @@ fun FilterCheckboxItem( /** * Extension-Funktion für MapViewModel - * Wendet Filter auf den FeatureLayer an + * Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG) */ -suspend fun MapViewModel.applyDamageFilter(selectedTypes: Set): Boolean { +suspend fun MapViewModel.applyDamageFilter( + selectedTypes: Set, + startDate: LocalDate? = null, + endDate: LocalDate? = null +): Boolean { return withContext(Dispatchers.IO) { try { - if (selectedTypes.isEmpty()) { - // Kein Filter - zeige alle Features - featureLayer.definitionExpression = "" - println("DEBUG: Filter entfernt - zeige alle Features") - } else { - // Erstelle WHERE-Klausel für SQL - // Beispiel: "Typ IN ('Straße', 'Gehweg')" - val typeList = selectedTypes.joinToString("', '", "'", "'") - val whereClause = "Typ IN ($typeList)" + val whereClauses = mutableListOf() - featureLayer.definitionExpression = whereClause - println("DEBUG: Filter angewendet: $whereClause") + // ===== TYP-FILTER ===== + // WICHTIG: Nur hinzufügen wenn nicht ALLE Typen ausgewählt sind + // Wenn alle ausgewählt sind, filtere nicht nach Typ (ermöglicht reinen Datumsfilter) + if (selectedTypes.isNotEmpty() && selectedTypes.size < DAMAGE_TYPES.size) { + val typeList = selectedTypes.joinToString("', '", "'", "'") + whereClauses.add("Typ IN ($typeList)") } + // ===== DATUMS-FILTER ===== + // Feldname: EditDate + if (startDate != null || endDate != null) { + val dateField = "EditDate" + + when { + startDate != null && endDate != null -> { + whereClauses.add("$dateField >= timestamp '$startDate 00:00:00' AND $dateField <= timestamp '$endDate 23:59:59'") + } + startDate != null -> { + whereClauses.add("$dateField >= timestamp '$startDate 00:00:00'") + } + endDate != null -> { + whereClauses.add("$dateField <= timestamp '$endDate 23:59:59'") + } + } + } + + // ===== ALLE KLAUSELN KOMBINIEREN ===== + val whereClause = whereClauses.joinToString(" AND ") + + featureLayer.definitionExpression = whereClause + println("DEBUG: Filter angewendet: '$whereClause'") + println("DEBUG: Typen: ${selectedTypes.size}/${DAMAGE_TYPES.size}, Datum aktiv: ${startDate != null || endDate != null}") + withContext(Dispatchers.Main) { - snackBarMessage = if (selectedTypes.isEmpty()) { - "Alle Schäden werden angezeigt" - } else { - "${selectedTypes.size} Typ(en) gefiltert" + snackBarMessage = when { + selectedTypes.isEmpty() && (startDate == null && endDate == null) -> { + "Alle Schäden werden angezeigt" + } + selectedTypes.size == DAMAGE_TYPES.size && (startDate != null || endDate != null) -> { + // NUR Datumsfilter aktiv (alle Typen ausgewählt) + "Datumsfilter: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}" + } + selectedTypes.size < DAMAGE_TYPES.size && (startDate == null && endDate == null) -> { + // NUR Typ-Filter aktiv + "${selectedTypes.size} Typ(en) gefiltert" + } + else -> { + // BEIDE Filter aktiv + val typeInfo = "${selectedTypes.size} Typ(en)" + val dateInfo = "Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}" + "$typeInfo | $dateInfo" + } } } @@ -235,14 +397,11 @@ suspend fun MapViewModel.applyDamageFilter(selectedTypes: Set): Boolean * Gibt die aktuell aktiven Filter zurück */ fun MapViewModel.getActiveFilters(): Set { - val expression = featureLayer.definitionExpression if (expression.isEmpty()) return emptySet() - val regex = "'([^']+)'".toRegex() return regex.findAll(expression) .map { it.groupValues[1] } .toSet() -} - +} \ No newline at end of file