Feedback-Window
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 50s

This commit is contained in:
Torsten Schulz (local)
2026-06-09 07:57:36 +02:00
parent f0142d5682
commit 16465fafc8
9 changed files with 208 additions and 3 deletions

View File

@@ -17,6 +17,7 @@ import de.tsschulz.tt_tagebuch.shared.api.ClubsApi
import de.tsschulz.tt_tagebuch.shared.api.DiaryApi
import de.tsschulz.tt_tagebuch.shared.api.DiaryMemberActivitiesApi
import de.tsschulz.tt_tagebuch.shared.api.DiaryMemberApi
import de.tsschulz.tt_tagebuch.shared.api.FeedbackApi
import de.tsschulz.tt_tagebuch.shared.api.GroupApi
import de.tsschulz.tt_tagebuch.shared.api.ParticipantsApi
import de.tsschulz.tt_tagebuch.shared.api.PredefinedActivitiesApi
@@ -88,6 +89,7 @@ class AppDependencies(context: Context) {
)
val publicAuthApi = PublicAuthApi(publicHttpClient)
val feedbackApi = FeedbackApi(client)
val authManager = AuthManager(
tokenProvider = tokenProvider,

View File

@@ -100,12 +100,14 @@ import androidx.compose.ui.unit.sp
import kotlin.math.max
import de.tsschulz.tt_tagebuch.app.AppDependencies
import de.tsschulz.tt_tagebuch.R
import de.tsschulz.tt_tagebuch.BuildConfig
import de.tsschulz.tt_tagebuch.app.pdf.sharePdfFile
import de.tsschulz.tt_tagebuch.app.pdf.writeMembersPhoneListPdf
import de.tsschulz.tt_tagebuch.app.pdf.writeTrainingDaySummaryPdf
import de.tsschulz.tt_tagebuch.app.pdf.writeTrainingPlanPdf
import de.tsschulz.tt_tagebuch.shared.api.memberProfileImagePath
import de.tsschulz.tt_tagebuch.shared.api.toAbsoluteUrl
import de.tsschulz.tt_tagebuch.shared.api.MobileFeedbackBody
import de.tsschulz.tt_tagebuch.shared.api.models.MemberGroupPhotoDto
import de.tsschulz.tt_tagebuch.shared.api.models.canReadApprovals
import de.tsschulz.tt_tagebuch.shared.api.models.canReadClubPermissions
@@ -296,6 +298,8 @@ private fun MainTabs(
val visibleTabs = visibleMainTabs(clubState.currentPermissions)
val networkConnected by dependencies.networkConnectivity.connected.collectAsState()
var lastNetworkConnected by remember { mutableStateOf(true) }
var feedbackOpen by remember { mutableStateOf(false) }
val currentScreenTitle = tabTitle(selectedTab)
/** Tagebuch- und Mitglieder-Listen vorladen, sobald Verein + Rechte feststehen. */
LaunchedEffect(clubState.currentClubId, clubState.currentPermissions) {
@@ -494,6 +498,10 @@ private fun MainTabs(
settingsStammdatenRequest = settingsStammdatenRequest,
onConsumeSettingsStammdatenRequest = { settingsStammdatenRequest = null },
)
MobileFeedbackButton(
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp),
onClick = { feedbackOpen = true },
)
}
}
} else {
@@ -519,6 +527,10 @@ private fun MainTabs(
settingsStammdatenRequest = settingsStammdatenRequest,
onConsumeSettingsStammdatenRequest = { settingsStammdatenRequest = null },
)
MobileFeedbackButton(
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp),
onClick = { feedbackOpen = true },
)
}
BottomNavigation(
backgroundColor = MaterialTheme.colors.surface,
@@ -547,6 +559,92 @@ private fun MainTabs(
}
}
}
if (feedbackOpen) {
MobileFeedbackDialog(
dependencies = dependencies,
screenTitle = currentScreenTitle,
onDismiss = { feedbackOpen = false },
)
}
}
@Composable
private fun MobileFeedbackButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
OutlinedButton(
onClick = onClick,
modifier = modifier.heightIn(min = 40.dp),
) {
Text("Feedback")
}
}
@Composable
private fun MobileFeedbackDialog(
dependencies: AppDependencies,
screenTitle: String,
onDismiss: () -> Unit,
) {
val clubState by dependencies.clubManager.state.collectAsState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
var message by remember { mutableStateOf("") }
var sending by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
AlertDialog(
onDismissRequest = { if (!sending) onDismiss() },
title = { Text("Feedback senden") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Seite: $screenTitle", style = MaterialTheme.typography.caption)
OutlinedTextField(
value = message,
onValueChange = { message = it },
label = { Text("Was funktioniert nicht oder könnte besser sein?") },
minLines = 5,
enabled = !sending,
modifier = Modifier.fillMaxWidth(),
)
error?.let { Text(it, color = MaterialTheme.colors.error) }
}
},
confirmButton = {
TextButton(
enabled = !sending && message.trim().isNotBlank(),
onClick = {
scope.launch {
sending = true
error = null
val result = runCatching {
dependencies.feedbackApi.send(
MobileFeedbackBody(
message = message.trim(),
screen = screenTitle,
clubId = clubState.currentClubId,
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
backendBaseUrl = BuildConfig.BACKEND_BASE_URL,
),
)
}
sending = false
if (result.isSuccess) {
Toast.makeText(context, "Feedback gesendet", Toast.LENGTH_SHORT).show()
onDismiss()
} else {
error = result.exceptionOrNull()?.message ?: "Feedback konnte nicht gesendet werden."
}
}
},
) { Text(if (sending) "Sende..." else "Senden") }
},
dismissButton = {
TextButton(enabled = !sending, onClick = onDismiss) { Text("Abbrechen") }
},
)
}
@Composable

View File

@@ -1,7 +1,7 @@
[versions]
# composeApp (Play Store / „Über die App“-Build)
appVersionCode = "24"
appVersionName = "1.7.4"
appVersionCode = "25"
appVersionName = "1.7.5"
agp = "9.2.1"
android-compileSdk = "35"
android-minSdk = "24"

View File

@@ -0,0 +1,26 @@
package de.tsschulz.tt_tagebuch.shared.api
import de.tsschulz.tt_tagebuch.shared.api.http.AuthedHttpClient
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import kotlinx.serialization.Serializable
@Serializable
data class MobileFeedbackBody(
val message: String,
val screen: String,
val clubId: Int? = null,
val appVersion: String = "",
val platform: String = "Android",
val backendBaseUrl: String = "",
)
class FeedbackApi(
private val client: AuthedHttpClient,
) {
suspend fun send(body: MobileFeedbackBody) {
client.http.post("/api/mobile-feedback") {
setBody(body)
}
}
}