- Bewertungsystem

- Filtern nach Schäden
- Docs Ordner mit Markdown dummies gefüllt.
This commit is contained in:
2026-01-21 21:37:58 +01:00
parent 33e95641d0
commit 05426b687c
21 changed files with 642 additions and 29 deletions

View File

@@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.AddLocationAlt
import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.filled.FormatListNumbered import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -48,6 +49,8 @@ import com.example.snapandsolve.camera.AlbumViewState
import com.example.snapandsolve.camera.Intent import com.example.snapandsolve.camera.Intent
import com.example.snapandsolve.ui.theme.* import com.example.snapandsolve.ui.theme.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun MainScreen(modifier: Modifier = Modifier, application: Application) { fun MainScreen(modifier: Modifier = Modifier, application: Application) {
@@ -124,15 +127,20 @@ fun ContentScreen(
sliderOpen: Boolean, sliderOpen: Boolean,
onDismissReport: () -> Unit onDismissReport: () -> Unit
) { ) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
// NEU: State für Filter-Dialog
var showFilterDialog by remember { mutableStateOf(false) }
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
// HINTERGRUND: Die Map // Map
MapSegment( MapSegment(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
mapViewModel = mapViewModel, mapViewModel = mapViewModel
) )
// VORDERGRUND: Das Overlay (wenn showReport = true) // Report Overlay
if (showReport) { if (showReport) {
ReportOverlay( ReportOverlay(
onCancel = onDismissReport, onCancel = onDismissReport,
@@ -142,15 +150,34 @@ fun ContentScreen(
) )
} }
if (mapViewModel.showFeatureInfo && mapViewModel.selectedFeature != null) { // Feature Info Dialog
FeatureInfoOverlay( if (mapViewModel.showFeatureInfo) {
feature = mapViewModel.selectedFeature!!, FeatureInfoDialog(
onClose = { mapViewModel.closeFeatureInfo() } feature = mapViewModel.selectedFeature,
onDismiss = { mapViewModel.closeFeatureInfo() },
onRate = { feature, isPositive ->
coroutineScope.launch {
mapViewModel.updateFeatureRating(feature, isPositive, context)
}
}
) )
} }
// Filter Dialog - NEU!
if (showFilterDialog) {
DamageFilterDialog(
damageTypes = MapViewModel.DAMAGE_TYPES, // <-- Nutzt zentrale Liste
currentFilters = mapViewModel.getActiveFilters(),
onDismiss = { showFilterDialog = false },
onApplyFilter = { selectedTypes ->
coroutineScope.launch {
mapViewModel.applyDamageFilter(selectedTypes)
}
}
)
}
// Slider von Links // Side Slider
SideSlider(visible = sliderOpen) { SideSlider(visible = sliderOpen) {
Text( Text(
"Menü", "Menü",
@@ -162,16 +189,14 @@ fun ContentScreen(
text = "Schäden filtern", text = "Schäden filtern",
icon = Icons.Default.FilterAlt, icon = Icons.Default.FilterAlt,
onClick = { onClick = {
/* TODO */ showFilterDialog = true // <-- Öffnet Filter-Dialog
} }
) )
SliderMenuItem( SliderMenuItem(
text = "Schadensliste", text = "Schadensliste",
icon = Icons.Default.FormatListNumbered, icon = Icons.Default.FormatListNumbered,
onClick = { onClick = { /* TODO */ }
/* TODO */
}
) )
} }
} }
@@ -275,7 +300,7 @@ fun ReportOverlay(
expanded = dropdownExpanded, expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false } onDismissRequest = { dropdownExpanded = false }
) { ) {
listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges").forEach { typ -> MapViewModel.DAMAGE_TYPES.forEach { typ -> // <-- Nutzt zentrale Liste
DropdownMenuItem( DropdownMenuItem(
text = { Text(typ) }, text = { Text(typ) },
onClick = { onClick = {

View File

@@ -9,10 +9,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -20,8 +18,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.arcgismaps.ApiKey import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.Color
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.location.LocationDisplayAutoPanMode import com.arcgismaps.location.LocationDisplayAutoPanMode
import com.arcgismaps.toolkit.geoviewcompose.MapView import com.arcgismaps.toolkit.geoviewcompose.MapView
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
@@ -31,6 +27,7 @@ import kotlinx.coroutines.launch
fun MapSegment( fun MapSegment(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
mapViewModel: MapViewModel mapViewModel: MapViewModel
) { ) {
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -64,8 +61,7 @@ fun MapSegment(
} }
Column( Column(
modifier = modifier modifier = modifier.fillMaxSize(),
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
MapView( MapView(
@@ -73,11 +69,9 @@ fun MapSegment(
arcGISMap = mapViewModel.map, arcGISMap = mapViewModel.map,
locationDisplay = locationDisplay, locationDisplay = locationDisplay,
mapViewProxy = mapViewModel.mapViewProxy, mapViewProxy = mapViewModel.mapViewProxy,
onSingleTapConfirmed = mapViewModel::onTap, onSingleTapConfirmed = mapViewModel::onTap, // Ganz normal
graphicsOverlays = listOf(mapViewModel.tempOverlay) graphicsOverlays = listOf(mapViewModel.tempOverlay)
) { )
/* TODO */
}
} }
} }

View File

@@ -33,11 +33,21 @@ import java.io.ByteArrayOutputStream
class MapViewModel(application: Application) : AndroidViewModel(application) { class MapViewModel(application: Application) : AndroidViewModel(application) {
companion object {
// Zentrale Definition der Schadenstypen
val DAMAGE_TYPES = listOf(
"Straße",
"Gehweg",
"Fahrradweg",
"Beleuchtung",
"Sonstiges"
)
}
val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply { val map: ArcGISMap = ArcGISMap(BasemapStyle.OpenOsmStyle).apply {
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0) initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
} }
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT) var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
private set
var reopenReport by mutableStateOf(false) var reopenReport by mutableStateOf(false)
private set private set
var showFeatureInfo by mutableStateOf(false) var showFeatureInfo by mutableStateOf(false)
@@ -64,7 +74,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
init { init {
tempOverlay.graphics.add(pointGraphic) tempOverlay.graphics.add(pointGraphic)
viewModelScope.launch { viewModelScope.launch {
serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/251120_StrassenSchaeden/FeatureServer/0") serviceFeatureTable = ServiceFeatureTable("https://services9.arcgis.com/UVxdrlZq3S3gqt7w/arcgis/rest/services/si_StrassenSchaeden/FeatureServer/0")
serviceFeatureTable.load().onSuccess { serviceFeatureTable.load().onSuccess {
val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" } val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
val attributeDomain = typeDamageField?.domain as? CodedValueDomain val attributeDomain = typeDamageField?.domain as? CodedValueDomain
@@ -175,7 +185,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
} }
fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) { fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
if (featureLayer.loadStatus.value != LoadStatus.Loaded) { if (featureLayer.loadStatus.value != LoadStatus.Loaded) {
snackBarMessage = "Layer not loaded!" snackBarMessage = "Layer not loaded!"
return return
@@ -187,7 +196,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(singleTapConfirmedEvent.screenCoordinate)
FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate) FeatureOperationType.PICK_REPORT_LOCATION -> pickReportLocation(singleTapConfirmedEvent.screenCoordinate)
else -> {} // RATE_FEATURE wird nicht mehr gebraucht - wird im DEFAULT mit behandelt!
} }
} }
@@ -250,7 +259,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
} }
// When a feature is selected, update its geometry to the tapped location.
else -> { else -> {
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint -> mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
// Normalize the point - needed when the tapped location is over the international date line. // Normalize the point - needed when the tapped location is over the international date line.

View File

@@ -0,0 +1,248 @@
package com.example.snapandsolve.ui.theme
import MapViewModel
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Dialog für Schaden-Filter
* Ermöglicht das Filtern von Features nach Typ
*/
@Composable
fun DamageFilterDialog(
damageTypes: List<String>,
currentFilters: Set<String>,
onDismiss: () -> Unit,
onApplyFilter: (Set<String>) -> Unit
) {
var selectedFilters by remember { mutableStateOf(currentFilters) }
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.7f),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Schäden filtern",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Schließen")
}
}
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
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
damageTypes.forEach { type ->
FilterCheckboxItem(
label = type,
isChecked = selectedFilters.contains(type),
onCheckedChange = { isChecked ->
selectedFilters = if (isChecked) {
selectedFilters + type
} else {
selectedFilters - type
}
}
)
}
}
Divider(modifier = Modifier.padding(vertical = 16.dp))
// Info über aktive Filter
if (selectedFilters.isNotEmpty()) {
Text(
text = "${selectedFilters.size} von ${damageTypes.size} Typen ausgewählt",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Alle auswählen
OutlinedButton(
onClick = { selectedFilters = damageTypes.toSet() },
modifier = Modifier.weight(1f)
) {
Text("Alle")
}
// Alle abwählen
OutlinedButton(
onClick = { selectedFilters = emptySet() },
modifier = Modifier.weight(1f)
) {
Text("Keine")
}
}
Spacer(modifier = Modifier.height(12.dp))
// Anwenden Button
Button(
onClick = {
onApplyFilter(selectedFilters)
onDismiss()
},
modifier = Modifier.fillMaxWidth()
) {
Text(
if (selectedFilters.isEmpty()) "Alle anzeigen"
else "Filter anwenden"
)
}
}
}
}
}
/**
* Checkbox-Item für einen Filter
*/
@Composable
fun FilterCheckboxItem(
label: String,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = isChecked,
onCheckedChange = onCheckedChange
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
Text(
text = when (label) {
"Straße" -> "🛣️"
"Gehweg" -> "🚶"
"Fahrradweg" -> "🚴"
"Beleuchtung" -> "💡"
"Sonstiges" -> "📍"
else -> ""
},
style = MaterialTheme.typography.headlineSmall
)
}
}
/**
* Extension-Funktion für MapViewModel
* Wendet Filter auf den FeatureLayer an
*/
suspend fun MapViewModel.applyDamageFilter(selectedTypes: Set<String>): 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)"
featureLayer.definitionExpression = whereClause
println("DEBUG: Filter angewendet: $whereClause")
}
withContext(Dispatchers.Main) {
snackBarMessage = if (selectedTypes.isEmpty()) {
"Alle Schäden werden angezeigt"
} else {
"${selectedTypes.size} Typ(en) gefiltert"
}
}
true
} catch (e: Exception) {
println("DEBUG: Fehler beim Anwenden des Filters: ${e.message}")
e.printStackTrace()
withContext(Dispatchers.Main) {
snackBarMessage = "Fehler beim Filtern: ${e.message}"
}
false
}
}
}
/**
* Extension-Funktion für MapViewModel
* 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()
}

View File

@@ -0,0 +1,337 @@
package com.example.snapandsolve
import MapViewModel
import android.content.Context
import android.graphics.BitmapFactory
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.arcgismaps.data.ArcGISFeature
import com.arcgismaps.data.Attachment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Composable Dialog für Feature-Details mit Bewertung
* Zeigt alle Attribute, Fotos und Bewertungsbuttons
*/
@Composable
fun FeatureInfoDialog(
feature: ArcGISFeature?,
onDismiss: () -> Unit,
onRate: (ArcGISFeature, Boolean) -> Unit
) {
if (feature == null) return
var attachments by remember { mutableStateOf<List<Attachment>>(emptyList()) }
var photoBitmaps by remember { mutableStateOf<List<androidx.compose.ui.graphics.ImageBitmap>>(emptyList()) }
var isLoadingPhotos by remember { mutableStateOf(true) }
// Lade Fotos beim Öffnen
LaunchedEffect(feature) {
try {
// Lade Feature falls nötig
if (feature.loadStatus.value != com.arcgismaps.LoadStatus.Loaded) {
feature.load().getOrNull()
}
// Hole Attachments
feature.fetchAttachments().onSuccess { fetchedAttachments ->
attachments = fetchedAttachments
// Lade Foto-Daten
val bitmaps = mutableListOf<androidx.compose.ui.graphics.ImageBitmap>()
fetchedAttachments.forEach { attachment ->
attachment.fetchData().onSuccess { data ->
try {
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
if (bitmap != null) {
bitmaps.add(bitmap.asImageBitmap())
}
} catch (e: Exception) {
println("DEBUG: Fehler beim Laden des Bildes: ${e.message}")
}
}
}
photoBitmaps = bitmaps
}
} catch (e: Exception) {
println("DEBUG: Fehler beim Laden der Attachments: ${e.message}")
} finally {
isLoadingPhotos = false
}
}
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.85f),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.verticalScroll(rememberScrollState())
) {
// Header mit Typ
Text(
text = feature.attributes["Typ"]?.toString() ?: "Straßenschaden",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
Divider(modifier = Modifier.padding(bottom = 16.dp))
// Beschreibung
val description = feature.attributes["Beschreibung"]?.toString()
if (!description.isNullOrEmpty()) {
Text(
text = "Beschreibung",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
}
// Fotos
if (isLoadingPhotos) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(8.dp))
Text("Lade Fotos...")
}
} else if (photoBitmaps.isNotEmpty()) {
Text(
text = "Fotos (${photoBitmaps.size})",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
photoBitmaps.forEach { bitmap ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Image(
bitmap = bitmap,
contentDescription = "Schadensfoto",
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp),
contentScale = ContentScale.Fit
)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
// Community-Bewertung
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Community-Bewertung",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold
)
Row(
modifier = Modifier.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "👥",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "$currentRating",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (currentRating == 1) "Bestätigung" else "Bestätigungen",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
// Info-Text
Text(
text = "Hast du diesen Schaden auch gesehen?",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 12.dp)
)
// Bewertungs-Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Daumen runter
OutlinedButton(
onClick = {
onRate(feature, false)
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 8.dp)
) {
Text("👎", style = MaterialTheme.typography.headlineMedium)
Text("Nein", style = MaterialTheme.typography.bodyMedium)
}
}
// Daumen hoch
Button(
onClick = {
onRate(feature, true)
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 8.dp)
) {
Text("👍", style = MaterialTheme.typography.headlineMedium)
Text("Ja", style = MaterialTheme.typography.bodyMedium)
}
}
}
// Schließen Button
TextButton(
onClick = onDismiss,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Text("Schließen")
}
}
}
}
}
/**
* Extension-Funktion für MapViewModel
* Aktualisiert die Community-Bewertung eines Features
*/
suspend fun MapViewModel.updateFeatureRating(
feature: ArcGISFeature,
isPositive: Boolean,
context: Context
): Boolean {
return withContext(Dispatchers.IO) {
try {
// Hole aktuelle Bewertung
val currentRating = (feature.attributes["communitycounter"] as? Number)?.toInt() ?: 0
// Berechne neue Bewertung
val newRating = if (isPositive) {
currentRating + 1
} else {
maxOf(0, currentRating - 1) // Verhindert negative Werte
}
println("DEBUG: Rating-Update: $currentRating -> $newRating (${if(isPositive) "+" else "-"})")
// Aktualisiere Feature-Attribut
feature.attributes["communitycounter"] = newRating
// Speichere in Feature Table
serviceFeatureTable.updateFeature(feature).onSuccess {
println("DEBUG: updateFeature erfolgreich")
// Synchronisiere mit ArcGIS Online
serviceFeatureTable.applyEdits().onSuccess {
println("DEBUG: applyEdits erfolgreich - Rating gespeichert")
withContext(Dispatchers.Main) {
val message = if (isPositive) {
"✓ Schaden bestätigt! (${currentRating}${newRating})"
} else {
"✓ Bewertung verringert (${currentRating}${newRating})"
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
snackBarMessage = message
}
}.onFailure { error ->
println("DEBUG: applyEdits fehlgeschlagen: ${error.message}")
withContext(Dispatchers.Main) {
Toast.makeText(context, "Sync-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
}
}
}.onFailure { error ->
println("DEBUG: updateFeature fehlgeschlagen: ${error.message}")
withContext(Dispatchers.Main) {
Toast.makeText(context, "Update-Fehler: ${error.message}", Toast.LENGTH_LONG).show()
}
}
true
} catch (e: Exception) {
println("DEBUG: Rating-Update Exception: ${e.message}")
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"Fehler beim Bewerten: ${e.message}",
Toast.LENGTH_LONG
).show()
}
false
}
}
}

View File

0
docs/AlbumEvents.md Normal file
View File

0
docs/AlbumViewModel.md Normal file
View File

0
docs/AlbumViewState.md Normal file
View File

0
docs/Color.md Normal file
View File

View File

View File

0
docs/MainActivity.md Normal file
View File

0
docs/MainScreen.md Normal file
View File

0
docs/MapSegment.md Normal file
View File

0
docs/MapViewModel.md Normal file
View File

0
docs/SideSlider.md Normal file
View File

0
docs/Theme.md Normal file
View File

0
docs/Type.md Normal file
View File

0
docs/Widget.md Normal file
View File

0
docs/locationHelper.md Normal file
View File