diff --git a/app/src/main/java/com/example/snapandsolve/MainScreen.kt b/app/src/main/java/com/example/snapandsolve/MainScreen.kt index 7b9b216..2ed594a 100644 --- a/app/src/main/java/com/example/snapandsolve/MainScreen.kt +++ b/app/src/main/java/com/example/snapandsolve/MainScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/example/snapandsolve/MapSegment.kt b/app/src/main/java/com/example/snapandsolve/MapSegment.kt index 9776a3e..321e422 100644 --- a/app/src/main/java/com/example/snapandsolve/MapSegment.kt +++ b/app/src/main/java/com/example/snapandsolve/MapSegment.kt @@ -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) { ) ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt index 7478835..8f1fb01 100644 --- a/app/src/main/java/com/example/snapandsolve/MapViewModel.kt +++ b/app/src/main/java/com/example/snapandsolve/MapViewModel.kt @@ -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. diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt new file mode 100644 index 0000000..6556ccc --- /dev/null +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/DamageFilterSystem.kt @@ -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, + currentFilters: Set, + onDismiss: () -> Unit, + onApplyFilter: (Set) -> 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): 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 { + + val expression = featureLayer.definitionExpression + if (expression.isEmpty()) return emptySet() + + + val regex = "'([^']+)'".toRegex() + return regex.findAll(expression) + .map { it.groupValues[1] } + .toSet() +} + diff --git a/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt b/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt new file mode 100644 index 0000000..6a52810 --- /dev/null +++ b/app/src/main/java/com/example/snapandsolve/ui/theme/FeatureRatingSystem.kt @@ -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>(emptyList()) } + var photoBitmaps by remember { mutableStateOf>(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() + 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 + } + } +} \ No newline at end of file diff --git a/docs/AlbumAndroidViewModel.md b/docs/AlbumAndroidViewModel.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/AlbumEvents.md b/docs/AlbumEvents.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/AlbumViewModel.md b/docs/AlbumViewModel.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/AlbumViewState.md b/docs/AlbumViewState.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Color.md b/docs/Color.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/DamageFilterSystem.md b/docs/DamageFilterSystem.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/FeatureRatingSystem.md b/docs/FeatureRatingSystem.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MainActivity.md b/docs/MainActivity.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MainScreen.md b/docs/MainScreen.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MapSegment.md b/docs/MapSegment.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MapViewModel.md b/docs/MapViewModel.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/SideSlider.md b/docs/SideSlider.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Theme.md b/docs/Theme.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Type.md b/docs/Type.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Widget.md b/docs/Widget.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/locationHelper.md b/docs/locationHelper.md new file mode 100644 index 0000000..e69de29