- Filtern nach Status

- App Meldungen über nahe Schäden erhalten.
This commit is contained in:
2026-02-08 12:35:11 +01:00
parent 7781551e02
commit 7d6bf0fdd7
9 changed files with 707 additions and 67 deletions

View File

@@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-25T17:20:21.835673100Z">
<DropdownSelection timestamp="2026-02-06T10:29:08.485527Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\sklau\.android\avd\Medium_Phone.avd" />

View File

@@ -10,6 +10,10 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -19,6 +23,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SnapAndSolve">
<activity
android:name="com.example.snapandsolve.MainActivity"
android:exported="true"
@@ -26,10 +31,17 @@
android:theme="@style/Theme.SnapAndSolve">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Service für Proximity-Benachrichtigungen -->
<service
android:name=".service.ProximityNotificationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@@ -1,5 +1,6 @@
package com.example.snapandsolve
import MainScreen
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

View File

@@ -1,5 +1,3 @@
package com.example.snapandsolve
import DamageFilterDialog
import DamageListDialog
import MapViewModel
@@ -10,6 +8,8 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -18,10 +18,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import applyDamageFilter
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.toolkit.geoviewcompose.MapView
import com.example.snapandsolve.FeatureInfoDialog
import com.example.snapandsolve.MapSegment
import com.example.snapandsolve.camera.AlbumViewModel
import com.example.snapandsolve.ui.theme.*
import com.example.snapandsolve.ui.theme.composable.SideSlider
import com.example.snapandsolve.ui.theme.composable.SliderMenuItem
import com.example.snapandsolve.updateFeatureRating
import com.example.snapandsolve.view.ReportDialog
import getActiveFilters
import kotlinx.coroutines.Dispatchers
@@ -32,6 +40,8 @@ import kotlinx.coroutines.launch
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
var showReport by rememberSaveable { mutableStateOf(false) }
var sliderOpen by rememberSaveable { mutableStateOf(false) }
var showSettings by rememberSaveable { mutableStateOf(false) }
val mapViewModel = remember { MapViewModel(application) }
val albumViewModel = remember { AlbumViewModel(Dispatchers.Default) }
@@ -44,6 +54,15 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
showReport = false
}
fun openSettings() {
showSettings = true
sliderOpen = false
}
fun closeSettings() {
showSettings = false
}
LaunchedEffect(mapViewModel.reopenReport) {
if (mapViewModel.reopenReport) {
showReport = true
@@ -51,7 +70,6 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { AppTopBar() },
@@ -60,7 +78,7 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
modifier = Modifier.height(120.dp),
containerColor = AppColor,
) {
IconButton(onClick = { sliderOpen = !sliderOpen; showReport = false }) {
IconButton(onClick = { sliderOpen = !sliderOpen; showReport = false; showSettings = false }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
}
@@ -80,14 +98,22 @@ fun MainScreen(modifier: Modifier = Modifier, application: Application) {
},
floatingActionButtonPosition = FabPosition.Center,
) { innerPadding ->
ContentScreen(
modifier = Modifier.padding(innerPadding),
mapViewModel = mapViewModel,
albumViewModel = albumViewModel,
showReport = showReport,
sliderOpen = sliderOpen,
onDismissReport = ::closeReport
)
if (showSettings) {
SettingsScreen(
onBack = ::closeSettings,
mapViewModel = mapViewModel
)
} else {
ContentScreen(
modifier = Modifier.padding(innerPadding),
mapViewModel = mapViewModel,
albumViewModel = albumViewModel,
showReport = showReport,
sliderOpen = sliderOpen,
onDismissReport = ::closeReport,
onOpenSettings = ::openSettings
)
}
}
}
@@ -98,14 +124,13 @@ fun ContentScreen(
albumViewModel: AlbumViewModel,
showReport: Boolean,
sliderOpen: Boolean,
onDismissReport: () -> Unit
onDismissReport: () -> Unit,
onOpenSettings: () -> Unit
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
// NEU: State für Filter-Dialog
var showFilterDialog by remember { mutableStateOf(false) }
var showDamageList by remember { mutableStateOf(false) }
Box(modifier = modifier.fillMaxSize()) {
@@ -150,15 +175,15 @@ fun ContentScreen(
)
}
// Filter Dialog - NEU!
// Filter Dialog - AKTUALISIERT mit Status-Parameter
if (showFilterDialog) {
DamageFilterDialog(
damageTypes = MapViewModel.DAMAGE_TYPES, // <-- Nutzt zentrale Liste
damageTypes = MapViewModel.DAMAGE_TYPES,
currentFilters = mapViewModel.getActiveFilters(),
onDismiss = { showFilterDialog = false },
onApplyFilter = { selectedTypes, startDate, endDate ->
onApplyFilter = { selectedTypes, selectedStatus, startDate, endDate ->
coroutineScope.launch {
mapViewModel.applyDamageFilter(selectedTypes, startDate, endDate)
mapViewModel.applyDamageFilter(selectedTypes, selectedStatus, startDate, endDate)
}
}
)
@@ -172,12 +197,16 @@ fun ContentScreen(
modifier = Modifier.padding(bottom = 12.dp)
)
SliderMenuItem(
text = "Einstellungen",
icon = Icons.Default.Settings,
onClick = onOpenSettings
)
SliderMenuItem(
text = "Schäden filtern",
icon = Icons.Default.FilterAlt,
onClick = {
showFilterDialog = true // <-- Öffnet Filter-Dialog
}
onClick = { showFilterDialog = true }
)
SliderMenuItem(
@@ -224,5 +253,4 @@ suspend fun loadFirstAttachmentBitmap(
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
return bitmap.asImageBitmap()
}
*/
*/

View File

@@ -423,6 +423,10 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
fun clearDuplicateDamages() {
duplicateDamages = emptyList()
}
fun getFeatureTable(): ServiceFeatureTable? {
return featureLayer?.featureTable as? ServiceFeatureTable
}
}
enum class FeatureOperationType(val operationName: String, val instruction: String) {

View File

@@ -0,0 +1,295 @@
package com.example.snapandsolve.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.QueryParameters
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.LinearUnitId
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.location.SystemLocationDataSource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class ProximityNotificationService : Service() {
private var locationDataSource: SystemLocationDataSource? = null
private var featureTable: ServiceFeatureTable? = null
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val notifiedFeatures = mutableSetOf<String>()
companion object {
const val CHANNEL_ID = "proximity_notifications"
const val NOTIFICATION_ID = 1001
const val FOREGROUND_SERVICE_ID = 1000
const val PROXIMITY_RADIUS_METERS = 100.0
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
private var sharedFeatureTable: ServiceFeatureTable? = null
fun start(context: Context, featureTable: ServiceFeatureTable) {
sharedFeatureTable = featureTable
val intent = Intent(context, ProximityNotificationService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
sharedFeatureTable = null
context.stopService(Intent(context, ProximityNotificationService::class.java))
}
}
override fun onCreate() {
super.onCreate()
Log.d("ProximityService", "Service onCreate()")
createNotificationChannel()
startForeground(FOREGROUND_SERVICE_ID, createForegroundNotification())
_isRunning.value = true
featureTable = sharedFeatureTable
if (featureTable == null) {
Log.e("ProximityService", "FeatureTable is NULL in onCreate!")
} else {
Log.d("ProximityService", "FeatureTable successfully assigned")
Log.d("ProximityService", "FeatureTable SpatialReference: ${featureTable?.spatialReference?.wkid}")
}
startLocationTracking()
}
private fun startLocationTracking() {
locationDataSource = SystemLocationDataSource().apply {
serviceScope.launch {
start().onSuccess {
Log.d("ProximityService", "Location tracking started")
locationChanged.collect { location ->
Log.d("ProximityService", "Location update: ${location.position.x}, ${location.position.y}")
checkProximityToDamages(location)
}
}.onFailure { error ->
Log.e("ProximityService", "Failed to start location tracking: ${error.message}")
}
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("ProximityService", "Service onDestroy()")
serviceScope.launch {
locationDataSource?.stop()
}
serviceScope.cancel()
_isRunning.value = false
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Straßenschaden-Benachrichtigungen",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Benachrichtigungen über Straßenschäden in Ihrer Nähe"
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private fun createForegroundNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Straßenschaden-Überwachung")
.setContentText("Überwacht Straßenschäden in Ihrer Nähe")
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private suspend fun checkProximityToDamages(location: com.arcgismaps.location.Location) {
Log.d("ProximityService", "Checking proximity at: ${location.position.x}, ${location.position.y}")
if (featureTable == null) {
Log.e("ProximityService", "FeatureTable is NULL! Cannot check proximity")
return
}
// GPS Position (WGS84)
val currentPoint = Point(
location.position.x,
location.position.y,
SpatialReference.wgs84()
)
Log.d("ProximityService", "Current point SR: ${currentPoint.spatialReference?.wkid}")
val queryParams = QueryParameters().apply {
whereClause = "1=1"
returnGeometry = true
}
try {
val queryResult = featureTable?.queryFeatures(queryParams)?.getOrNull()
val features = queryResult?.map { it as ArcGISFeature } ?: emptyList()
Log.d("ProximityService", "Found ${features.size} features total")
features.forEach { feature ->
val featureGeometry = feature.geometry as? Point ?: return@forEach
Log.d("ProximityService", "Feature point SR: ${featureGeometry.spatialReference?.wkid}")
// KRITISCHER FIX: Beide Punkte ins gleiche Koordinatensystem bringen
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
} else {
currentPoint
}
if (projectedCurrentPoint == null) {
Log.e("ProximityService", "Failed to project current point")
return@forEach
}
// KORRIGIERTE Distanzberechnung mit GeometryEngine
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
point1 = projectedCurrentPoint,
point2 = featureGeometry,
distanceUnit = LinearUnit(LinearUnitId.Meters),
azimuthUnit = null,
curveType = GeodeticCurveType.Geodesic
)
val distance = distanceResult?.distance ?: return@forEach
Log.d("ProximityService", "Feature distance: ${String.format("%.2f", distance)}m")
if (distance <= PROXIMITY_RADIUS_METERS) {
val featureId = feature.attributes["OBJECTID"]?.toString() ?: return@forEach
if (!notifiedFeatures.contains(featureId)) {
Log.d("ProximityService", "Sending notification for feature $featureId at ${String.format("%.2f", distance)}m")
notifiedFeatures.add(featureId)
sendDamageNotification(feature, distance)
} else {
Log.d("ProximityService", "Already notified for feature $featureId")
}
}
}
cleanupNotifiedFeatures(currentPoint, features)
} catch (e: Exception) {
Log.e("ProximityService", "Error checking proximity", e)
e.printStackTrace()
}
}
private fun sendDamageNotification(feature: ArcGISFeature, distance: Double) {
// Versuche verschiedene Attribut-Namen für die Kategorie
val kategorie = feature.attributes["Kategorie"]?.toString()
?: feature.attributes["kategorie"]?.toString()
?: feature.attributes["Category"]?.toString()
?: feature.attributes["category"]?.toString()
?: "Straßenschaden" // Fallback
// Versuche verschiedene Attribut-Namen für die Beschreibung
val beschreibung = feature.attributes["Beschreibung"]?.toString()
?: feature.attributes["beschreibung"]?.toString()
?: feature.attributes["Description"]?.toString()
?: feature.attributes["description"]?.toString()
val distanceText = String.format("%.0f", distance)
// Baue den Notification-Text
val notificationText = if (beschreibung != null && beschreibung.isNotEmpty()) {
"$kategorie: $beschreibung - ca. ${distanceText}m entfernt"
} else {
"$kategorie - ca. ${distanceText}m entfernt"
}
Log.d("ProximityService", "Notification text: $notificationText")
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("⚠️ Straßenschaden in Ihrer Nähe!")
.setContentText(notificationText)
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationText)) // Für längere Texte
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build()
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationId = feature.attributes["OBJECTID"]?.toString()?.hashCode() ?: NOTIFICATION_ID
Log.d("ProximityService", "Displaying notification for: $kategorie")
notificationManager.notify(notificationId, notification)
}
private fun cleanupNotifiedFeatures(currentPoint: Point, features: List<ArcGISFeature>) {
val featuresToRemove = mutableListOf<String>()
notifiedFeatures.forEach { featureId ->
val feature = features.find { (it.attributes["OBJECTID"]?.toString() ?: "") == featureId }
if (feature == null) {
featuresToRemove.add(featureId)
return@forEach
}
val featureGeometry = feature.geometry as? Point ?: return@forEach
val projectedCurrentPoint = if (currentPoint.spatialReference?.wkid != featureGeometry.spatialReference?.wkid) {
GeometryEngine.projectOrNull(currentPoint, featureGeometry.spatialReference!!) as? Point
} else {
currentPoint
}
if (projectedCurrentPoint == null) return@forEach
val distanceResult = GeometryEngine.distanceGeodeticOrNull(
point1 = projectedCurrentPoint,
point2 = featureGeometry,
distanceUnit = LinearUnit(LinearUnitId.Meters),
azimuthUnit = null,
curveType = GeodeticCurveType.Geodesic
)
val distance = distanceResult?.distance ?: return@forEach
if (distance > PROXIMITY_RADIUS_METERS + 50) {
featuresToRemove.add(featureId)
Log.d("ProximityService", "Removing feature $featureId from cache (distance: ${String.format("%.2f", distance)}m)")
}
}
notifiedFeatures.removeAll(featuresToRemove)
}
}

View File

@@ -25,7 +25,8 @@ import java.time.format.DateTimeFormatter
fun FilterCheckboxItem(
label: String,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
onCheckedChange: (Boolean) -> Unit,
emoji: String = ""
) {
Row(
modifier = Modifier
@@ -47,14 +48,7 @@ fun FilterCheckboxItem(
)
Text(
text = when (label) {
"Straße" -> "🛣️"
"Gehweg" -> "🚶"
"Fahrradweg" -> "🚴"
"Beleuchtung" -> "💡"
"Sonstiges" -> "📍"
else -> ""
},
text = emoji,
style = MaterialTheme.typography.headlineSmall
)
}
@@ -62,22 +56,28 @@ fun FilterCheckboxItem(
/**
* Dialog für Schaden-Filter
* Ermöglicht das Filtern von Features nach Typ UND Datum (unabhängig voneinander)
* Ermöglicht das Filtern von Features nach Typ, Status UND Datum (unabhängig voneinander)
*/
@Composable
fun DamageFilterDialog(
damageTypes: List<String>,
currentFilters: Set<String>,
onDismiss: () -> Unit,
onApplyFilter: (Set<String>, LocalDate?, LocalDate?) -> Unit
onApplyFilter: (Set<String>, Set<String>, LocalDate?, LocalDate?) -> Unit
) {
// Status-Liste
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
// 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
)
}
// Status-Filter: Standardmäßig alle ausgewählt
var selectedStatus by remember { mutableStateOf(statusTypes.toSet()) }
var startDateString by remember { mutableStateOf("") }
var endDateString by remember { mutableStateOf("") }
var startDate by remember { mutableStateOf<LocalDate?>(null) }
@@ -147,6 +147,51 @@ fun DamageFilterDialog(
} else {
selectedFilters - type
}
},
emoji = when (type) {
"Straße" -> "🛣️"
"Gehweg" -> "🚶"
"Fahrradweg" -> "🚴"
"Beleuchtung" -> "💡"
"Sonstiges" -> "📍"
else -> ""
}
)
}
Divider(modifier = Modifier.padding(vertical = 16.dp))
// ===== STATUS-FILTER =====
Text(
text = "Bearbeitungsstatus:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp)
)
Text(
text = "Wähle die Status aus, die angezeigt werden sollen:",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
// Status-Liste
statusTypes.forEach { status ->
FilterCheckboxItem(
label = status,
isChecked = selectedStatus.contains(status),
onCheckedChange = { isChecked ->
selectedStatus = if (isChecked) {
selectedStatus + status
} else {
selectedStatus - status
}
},
emoji = when (status) {
"neu" -> "🔴"
"in Bearbeitung" -> "🟠"
"Schaden behoben" -> "🟢"
else -> ""
}
)
}
@@ -246,17 +291,26 @@ fun DamageFilterDialog(
Divider(modifier = Modifier.padding(vertical = 16.dp))
// Info über aktive Filter
if (selectedFilters.isNotEmpty() || useDateFilter) {
if (selectedFilters.isNotEmpty() || selectedStatus.size < statusTypes.size || useDateFilter) {
Text(
text = buildString {
// Typ-Info
if (selectedFilters.size < damageTypes.size) {
append("${selectedFilters.size} von ${damageTypes.size} Typ(en)")
} else {
append("Alle Typen")
}
// Status-Info
if (selectedStatus.size < statusTypes.size) {
append(" | ${selectedStatus.size} von ${statusTypes.size} Status")
} else {
append(" | Alle Status")
}
// Datum-Info
if (useDateFilter) {
if (selectedFilters.isNotEmpty()) append(" | ")
append("Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}")
append(" | Datum: ${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}")
}
},
style = MaterialTheme.typography.bodySmall,
@@ -268,22 +322,28 @@ fun DamageFilterDialog(
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Alle auswählen
// Alle Typen
OutlinedButton(
onClick = { selectedFilters = damageTypes.toSet() },
onClick = {
selectedFilters = damageTypes.toSet()
selectedStatus = statusTypes.toSet()
},
modifier = Modifier.weight(1f)
) {
Text("Alle Typen")
Text("Alle", maxLines = 1)
}
// Alle abwählen
// Keine Typen
OutlinedButton(
onClick = { selectedFilters = emptySet() },
onClick = {
selectedFilters = emptySet()
selectedStatus = emptySet()
},
modifier = Modifier.weight(1f)
) {
Text("Keine")
Text("Keine", maxLines = 1)
}
}
@@ -294,14 +354,14 @@ fun DamageFilterDialog(
onClick = {
val startDateToApply = if (useDateFilter) startDate else null
val endDateToApply = if (useDateFilter) endDate else null
onApplyFilter(selectedFilters, startDateToApply, endDateToApply)
onApplyFilter(selectedFilters, selectedStatus, startDateToApply, endDateToApply)
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
when {
selectedFilters.isEmpty() && !useDateFilter -> "Alle anzeigen"
selectedFilters.isEmpty() && selectedStatus.isEmpty() && !useDateFilter -> "Alle anzeigen"
else -> "Filter anwenden"
}
)
@@ -313,25 +373,34 @@ fun DamageFilterDialog(
/**
* Extension-Funktion für MapViewModel
* Wendet Filter auf den FeatureLayer an (Typ + Datum - UNABHÄNGIG)
* Wendet Filter auf den FeatureLayer an (Typ + Status + Datum - UNABHÄNGIG)
*/
suspend fun MapViewModel.applyDamageFilter(
selectedTypes: Set<String>,
selectedStatus: Set<String>,
startDate: LocalDate? = null,
endDate: LocalDate? = null
): Boolean {
val statusTypes = listOf("neu", "in Bearbeitung", "Schaden behoben")
return withContext(Dispatchers.IO) {
try {
val whereClauses = mutableListOf<String>()
// ===== 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)")
}
// ===== STATUS-FILTER =====
// Nur hinzufügen wenn nicht ALLE Status ausgewählt sind
if (selectedStatus.isNotEmpty() && selectedStatus.size < statusTypes.size) {
val statusList = selectedStatus.joinToString("', '", "'", "'")
whereClauses.add("status IN ($statusList)")
}
// ===== DATUMS-FILTER =====
// Feldname: EditDate
if (startDate != null || endDate != null) {
@@ -355,26 +424,32 @@ suspend fun MapViewModel.applyDamageFilter(
featureLayer.definitionExpression = whereClause
println("DEBUG: Filter angewendet: '$whereClause'")
println("DEBUG: Typen: ${selectedTypes.size}/${DAMAGE_TYPES.size}, Datum aktiv: ${startDate != null || endDate != null}")
println("DEBUG: Typen: ${selectedTypes.size}/${DAMAGE_TYPES.size}, Status: ${selectedStatus.size}/${statusTypes.size}, Datum aktiv: ${startDate != null || endDate != null}")
withContext(Dispatchers.Main) {
snackBarMessage = when {
selectedTypes.isEmpty() && (startDate == null && endDate == null) -> {
"Alle Schäden werden angezeigt"
snackBarMessage = buildString {
val parts = mutableListOf<String>()
// Typ-Info
if (selectedTypes.size < DAMAGE_TYPES.size) {
parts.add("${selectedTypes.size} Typ(en)")
}
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")) ?: "?"}"
// Status-Info
if (selectedStatus.size < statusTypes.size) {
parts.add("${selectedStatus.size} Status")
}
selectedTypes.size < DAMAGE_TYPES.size && (startDate == null && endDate == null) -> {
// NUR Typ-Filter aktiv
"${selectedTypes.size} Typ(en) gefiltert"
// Datum-Info
if (startDate != null || endDate != null) {
val dateStr = "${startDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"} - ${endDate?.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) ?: "?"}"
parts.add("Datum: $dateStr")
}
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"
if (parts.isEmpty()) {
append("Alle Schäden werden angezeigt")
} else {
append("Filter: ${parts.joinToString(" | ")}")
}
}
}

View File

@@ -0,0 +1,215 @@
package com.example.snapandsolve.ui.theme
import android.Manifest
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.*
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.unit.dp
import com.example.snapandsolve.service.ProximityNotificationService // KORREKTER IMPORT
import MapViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBack: () -> Unit,
mapViewModel: MapViewModel
) {
val context = LocalContext.current
val isProximityActive by ProximityNotificationService.isRunning.collectAsState()
// NEU: Notification Permission Launcher
val notificationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Toast.makeText(context, "Benachrichtigungen aktiviert", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Benachrichtigungen verweigert", Toast.LENGTH_SHORT).show()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Einstellungen") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Zurück"
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = AppColor,
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
// Benachrichtigungen Section
Text(
text = "Benachrichtigungen",
style = MaterialTheme.typography.titleMedium,
color = Color.Gray,
modifier = Modifier.padding(bottom = 8.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = WidgetColor
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = "Benachrichtigungen",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "Straßenschäden in der Nähe",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Benachrichtigung bei Schäden im Umkreis von 100m",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
modifier = Modifier.padding(top = 4.dp)
)
}
}
Switch(
checked = isProximityActive,
onCheckedChange = { enabled ->
if (enabled) {
// NEU: Prüfe Notification Permission (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val hasPermission = context.checkSelfPermission(
Manifest.permission.POST_NOTIFICATIONS
) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
return@Switch
}
}
mapViewModel.getFeatureTable()?.let { table ->
ProximityNotificationService.start(context, table)
} ?: run {
Toast.makeText(context, "Feature Table nicht verfügbar", Toast.LENGTH_SHORT).show()
}
} else {
ProximityNotificationService.stop(context)
}
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = ButtonColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = Color.LightGray
)
)
}
}
// Status-Anzeige
if (isProximityActive) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
colors = CardDefaults.cardColors(
containerColor = ButtonColor.copy(alpha = 0.1f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "",
color = ButtonColor,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = "Benachrichtigungen sind aktiv",
style = MaterialTheme.typography.bodyMedium,
color = ButtonColor
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
// Weitere Einstellungen können hier hinzugefügt werden
Text(
text = "Informationen",
style = MaterialTheme.typography.titleMedium,
color = Color.Gray,
modifier = Modifier.padding(bottom = 8.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = WidgetColor
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Proximity-Radius",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "100 Meter",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
</vector>