- Filterfunktion erweitert mit Filtern nach Datum

This commit is contained in:
2026-01-29 22:38:02 +01:00
parent fbf677c23a
commit d05da838a8
3 changed files with 235 additions and 52 deletions

View File

@@ -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)
}
}
)

View File

@@ -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()
}

View File

@@ -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<String>,
currentFilters: Set<String>,
onDismiss: () -> Unit,
onApplyFilter: (Set<String>) -> Unit
onApplyFilter: (Set<String>, 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<LocalDate?>(null) }
var endDate by remember { mutableStateOf<LocalDate?>(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<String>): Boolean {
suspend fun MapViewModel.applyDamageFilter(
selectedTypes: Set<String>,
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<String>()
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<String>): Boolean
* Gibt die aktuell aktiven Filter zurück
*/
fun MapViewModel.getActiveFilters(): Set<String> {
val expression = featureLayer.definitionExpression
if (expression.isEmpty()) return emptySet()
val regex = "'([^']+)'".toRegex()
return regex.findAll(expression)
.map { it.groupValues[1] }
.toSet()
}
}