- Bewertungsystem
- Filtern nach Schäden - Docs Ordner mit Markdown dummies gefüllt.
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.AddLocationAlt
|
||||
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.ThumbUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.ui.theme.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun MainScreen(modifier: Modifier = Modifier, application: Application) {
|
||||
@@ -124,15 +127,20 @@ fun ContentScreen(
|
||||
sliderOpen: Boolean,
|
||||
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()) {
|
||||
// HINTERGRUND: Die Map
|
||||
// Map
|
||||
MapSegment(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
mapViewModel = mapViewModel,
|
||||
mapViewModel = mapViewModel
|
||||
)
|
||||
|
||||
// VORDERGRUND: Das Overlay (wenn showReport = true)
|
||||
// Report Overlay
|
||||
if (showReport) {
|
||||
ReportOverlay(
|
||||
onCancel = onDismissReport,
|
||||
@@ -142,15 +150,34 @@ fun ContentScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (mapViewModel.showFeatureInfo && mapViewModel.selectedFeature != null) {
|
||||
FeatureInfoOverlay(
|
||||
feature = mapViewModel.selectedFeature!!,
|
||||
onClose = { mapViewModel.closeFeatureInfo() }
|
||||
// Feature Info Dialog
|
||||
if (mapViewModel.showFeatureInfo) {
|
||||
FeatureInfoDialog(
|
||||
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) {
|
||||
Text(
|
||||
"Menü",
|
||||
@@ -162,16 +189,14 @@ fun ContentScreen(
|
||||
text = "Schäden filtern",
|
||||
icon = Icons.Default.FilterAlt,
|
||||
onClick = {
|
||||
/* TODO */
|
||||
showFilterDialog = true // <-- Öffnet Filter-Dialog
|
||||
}
|
||||
)
|
||||
|
||||
SliderMenuItem(
|
||||
text = "Schadensliste",
|
||||
icon = Icons.Default.FormatListNumbered,
|
||||
onClick = {
|
||||
/* TODO */
|
||||
}
|
||||
onClick = { /* TODO */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -275,7 +300,7 @@ fun ReportOverlay(
|
||||
expanded = dropdownExpanded,
|
||||
onDismissRequest = { dropdownExpanded = false }
|
||||
) {
|
||||
listOf("Straße", "Gehweg", "Fahrradweg", "Beleuchtung", "Sonstiges").forEach { typ ->
|
||||
MapViewModel.DAMAGE_TYPES.forEach { typ -> // <-- Nutzt zentrale Liste
|
||||
DropdownMenuItem(
|
||||
text = { Text(typ) },
|
||||
onClick = {
|
||||
|
||||
@@ -9,10 +9,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -20,8 +18,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.arcgismaps.ApiKey
|
||||
import com.arcgismaps.ArcGISEnvironment
|
||||
import com.arcgismaps.Color
|
||||
import com.arcgismaps.data.ArcGISFeature
|
||||
import com.arcgismaps.location.LocationDisplayAutoPanMode
|
||||
import com.arcgismaps.toolkit.geoviewcompose.MapView
|
||||
import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
|
||||
@@ -31,6 +27,7 @@ import kotlinx.coroutines.launch
|
||||
fun MapSegment(
|
||||
modifier: Modifier = Modifier,
|
||||
mapViewModel: MapViewModel
|
||||
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -64,8 +61,7 @@ fun MapSegment(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
MapView(
|
||||
@@ -73,11 +69,9 @@ fun MapSegment(
|
||||
arcGISMap = mapViewModel.map,
|
||||
locationDisplay = locationDisplay,
|
||||
mapViewProxy = mapViewModel.mapViewProxy,
|
||||
onSingleTapConfirmed = mapViewModel::onTap,
|
||||
onSingleTapConfirmed = mapViewModel::onTap, // Ganz normal
|
||||
graphicsOverlays = listOf(mapViewModel.tempOverlay)
|
||||
) {
|
||||
/* TODO */
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,4 +119,4 @@ fun RequestPermissions(context: Context, onPermissionsGranted: () -> Unit) {
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,21 @@ import java.io.ByteArrayOutputStream
|
||||
|
||||
|
||||
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 {
|
||||
initialViewpoint = Viewpoint(53.14, 8.20, 20000.0)
|
||||
}
|
||||
var selectedOperation by mutableStateOf(FeatureOperationType.DEFAULT)
|
||||
private set
|
||||
var reopenReport by mutableStateOf(false)
|
||||
private set
|
||||
var showFeatureInfo by mutableStateOf(false)
|
||||
@@ -64,7 +74,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
init {
|
||||
tempOverlay.graphics.add(pointGraphic)
|
||||
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 {
|
||||
val typeDamageField = serviceFeatureTable.fields.firstOrNull { it.name == "Typ" }
|
||||
val attributeDomain = typeDamageField?.domain as? CodedValueDomain
|
||||
@@ -175,7 +185,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
fun onTap(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
|
||||
|
||||
if (featureLayer.loadStatus.value != LoadStatus.Loaded) {
|
||||
snackBarMessage = "Layer not loaded!"
|
||||
return
|
||||
@@ -187,7 +196,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
FeatureOperationType.UPDATE_ATTRIBUTE -> selectFeatureForAttributeEditAt(singleTapConfirmedEvent.screenCoordinate)
|
||||
FeatureOperationType.UPDATE_GEOMETRY -> updateFeatureGeometryAt(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 -> {
|
||||
mapViewProxy.screenToLocationOrNull(screenCoordinate)?.let { mapPoint ->
|
||||
// Normalize the point - needed when the tapped location is over the international date line.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
0
docs/AlbumAndroidViewModel.md
Normal file
0
docs/AlbumAndroidViewModel.md
Normal file
0
docs/AlbumEvents.md
Normal file
0
docs/AlbumEvents.md
Normal file
0
docs/AlbumViewModel.md
Normal file
0
docs/AlbumViewModel.md
Normal file
0
docs/AlbumViewState.md
Normal file
0
docs/AlbumViewState.md
Normal file
0
docs/Color.md
Normal file
0
docs/Color.md
Normal file
0
docs/DamageFilterSystem.md
Normal file
0
docs/DamageFilterSystem.md
Normal file
0
docs/FeatureRatingSystem.md
Normal file
0
docs/FeatureRatingSystem.md
Normal file
0
docs/MainActivity.md
Normal file
0
docs/MainActivity.md
Normal file
0
docs/MainScreen.md
Normal file
0
docs/MainScreen.md
Normal file
0
docs/MapSegment.md
Normal file
0
docs/MapSegment.md
Normal file
0
docs/MapViewModel.md
Normal file
0
docs/MapViewModel.md
Normal file
0
docs/SideSlider.md
Normal file
0
docs/SideSlider.md
Normal file
0
docs/Theme.md
Normal file
0
docs/Theme.md
Normal file
0
docs/Type.md
Normal file
0
docs/Type.md
Normal file
0
docs/Widget.md
Normal file
0
docs/Widget.md
Normal file
0
docs/locationHelper.md
Normal file
0
docs/locationHelper.md
Normal file
Reference in New Issue
Block a user