Enhance Veranstalter and Veranstaltung flows: add confirm dialog for event creation, refine navigation logic, and improve onboarding with keyboard focus handling.
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled

This commit is contained in:
2026-04-02 00:40:43 +02:00
parent b990f4dc05
commit d3d80f6995
11 changed files with 587 additions and 120 deletions
@@ -21,10 +21,5 @@ kotlin {
implementation(kotlin("test")) implementation(kotlin("test"))
} }
} }
val jvmTest by getting {
dependencies {
implementation(projects.platform.platformTesting)
}
}
} }
} }
@@ -72,7 +72,7 @@ class AltersklasseRechnerTest {
} }
@Test @Test
fun `ermittleAltersklassen berücksichtigt SpartenFilter`() { fun `ermittleAltersklassen beruecksichtigt SpartenFilter`() {
val reiter = DomReiter( val reiter = DomReiter(
personId = Uuid.random(), personId = Uuid.random(),
satznummer = "123456", satznummer = "123456",
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

@@ -17,6 +17,9 @@ private class FakeNav : NavigationPort {
override fun navigateToScreen(screen: AppScreen) { override fun navigateToScreen(screen: AppScreen) {
last = screen.route last = screen.route
} }
override fun navigateBack() {
// no-op for tests
}
} }
private class FakeUserProvider(private val user: User?) : CurrentUserProvider { private class FakeUserProvider(private val user: User?) : CurrentUserProvider {
@@ -35,8 +35,7 @@ enum class Sparte(val label: String) {
SPRINGEN("Springen"), SPRINGEN("Springen"),
VIELSEITIGKEIT("Vielseitigkeit"), VIELSEITIGKEIT("Vielseitigkeit"),
VOLTIGIEREN("Voltigieren"), VOLTIGIEREN("Voltigieren"),
FAHREN("Fahren"), FAHREN("Fahren")
REINING("Reining")
} }
enum class ReiterStatus(val label: String, val color: Color) { enum class ReiterStatus(val label: String, val color: Color) {
@@ -5,6 +5,8 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.singleWindowApplication import androidx.compose.ui.window.singleWindowApplication
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
/** /**
* Hot-Reload Preview Entry Point * Hot-Reload Preview Entry Point
@@ -27,7 +29,7 @@ private fun PreviewContent() {
Surface { Surface {
// --- REITER --- // --- REITER ---
// ReiterScreen(viewModel = ReiterViewModel()) ReiterScreen(viewModel = ReiterViewModel())
// --- PFERDE --- // --- PFERDE ---
// PferdeScreen(viewModel = PferdeViewModel()) // PferdeScreen(viewModel = PferdeViewModel())
@@ -11,8 +11,11 @@ import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -56,6 +59,10 @@ fun DesktopMainLayout(
onBack: () -> Unit, onBack: () -> Unit,
onLogout: () -> Unit, onLogout: () -> Unit,
) { ) {
// Onboarding-Eingaben zwischen Navigationswechseln behalten → State hier (außerhalb des when) hosten
var obGeraet by rememberSaveable { mutableStateOf("") }
var obKey by rememberSaveable { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
DesktopTopBar( DesktopTopBar(
currentScreen = currentScreen, currentScreen = currentScreen,
@@ -69,6 +76,10 @@ fun DesktopMainLayout(
currentScreen = currentScreen, currentScreen = currentScreen,
onNavigate = onNavigate, onNavigate = onNavigate,
onBack = onBack, onBack = onBack,
obGeraet = obGeraet,
obKey = obKey,
onObGeraetChange = { obGeraet = it },
onObKeyChange = { obKey = it },
) )
} }
DesktopFooterBar() DesktopFooterBar()
@@ -102,8 +113,8 @@ private fun DesktopTopBar(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
// Zurück-Pfeil (nur wenn nicht Root) // Zurück-Pfeil: für alle außer Onboarding anzeigen (damit man von "Verwaltung" zurück kommt)
if (currentScreen !is AppScreen.VeranstaltungVerwaltung) { if (currentScreen !is AppScreen.Onboarding) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück", contentDescription = "Zurück",
@@ -138,10 +149,10 @@ private fun DesktopTopBar(
is AppScreen.VeranstalterNeu -> { is AppScreen.VeranstalterNeu -> {
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
text = "Veranstalter auswählen", text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f), color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) }, modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
) )
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
@@ -154,10 +165,10 @@ private fun DesktopTopBar(
is AppScreen.VeranstalterDetail -> { is AppScreen.VeranstalterDetail -> {
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
text = "Veranstalter auswählen", text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f), color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) }, modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
) )
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
@@ -170,10 +181,10 @@ private fun DesktopTopBar(
is AppScreen.VeranstaltungProfil -> { is AppScreen.VeranstaltungProfil -> {
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
text = "Veranstalter auswählen", text = "Veranstalter-Verwaltung",
color = TopBarTextColor.copy(alpha = 0.75f), color = TopBarTextColor.copy(alpha = 0.75f),
fontSize = 14.sp, fontSize = 14.sp,
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterAuswahl) }, modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
) )
BreadcrumbSeparator() BreadcrumbSeparator()
Text( Text(
@@ -335,12 +346,21 @@ private fun DesktopContentArea(
currentScreen: AppScreen, currentScreen: AppScreen,
onNavigate: (AppScreen) -> Unit, onNavigate: (AppScreen) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
obGeraet: String,
obKey: String,
onObGeraetChange: (String) -> Unit,
onObKeyChange: (String) -> Unit,
) { ) {
when (currentScreen) { when (currentScreen) {
// Onboarding ohne Login // Onboarding ohne Login
is AppScreen.Onboarding -> { is AppScreen.Onboarding -> {
val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject() val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject()
at.mocode.desktop.v2.OnboardingScreen { _, _ -> at.mocode.desktop.v2.OnboardingScreen(
geraetName = obGeraet,
secureKey = obKey,
onGeraetNameChange = onObGeraetChange,
onSecureKeyChange = onObKeyChange,
) { _, _ ->
authTokenManager.setToken("dummy.jwt.token") authTokenManager.setToken("dummy.jwt.token")
onNavigate(AppScreen.VeranstaltungVerwaltung) onNavigate(AppScreen.VeranstaltungVerwaltung)
} }
@@ -373,11 +393,9 @@ private fun DesktopContentArea(
onEdit = { onNavigate(AppScreen.PferdProfil(it)) } onEdit = { onNavigate(AppScreen.PferdProfil(it)) }
) )
is AppScreen.PferdProfil -> PlaceholderScreen( is AppScreen.PferdProfil -> at.mocode.desktop.v2.PferdProfilV2(
"Pferde-Profil #${currentScreen.id}", id = currentScreen.id,
onBack = onBack, onBack = onBack,
onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
actionLabel = "Zurück zur Zentrale"
) )
// --- Reiter-Verwaltung & Profil --- // --- Reiter-Verwaltung & Profil ---
@@ -386,11 +404,9 @@ private fun DesktopContentArea(
onEdit = { onNavigate(AppScreen.ReiterProfil(it)) } onEdit = { onNavigate(AppScreen.ReiterProfil(it)) }
) )
is AppScreen.ReiterProfil -> PlaceholderScreen( is AppScreen.ReiterProfil -> at.mocode.desktop.v2.ReiterProfilV2(
"Reiter-Profil #${currentScreen.id}", id = currentScreen.id,
onBack = onBack, onBack = onBack,
onAction = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
actionLabel = "Zurück zur Zentrale"
) )
// --- Verein-Verwaltung & Profil --- // --- Verein-Verwaltung & Profil ---
@@ -399,11 +415,9 @@ private fun DesktopContentArea(
onEdit = { onNavigate(AppScreen.VereinProfil(it)) } onEdit = { onNavigate(AppScreen.VereinProfil(it)) }
) )
is AppScreen.VereinProfil -> PlaceholderScreen( is AppScreen.VereinProfil -> at.mocode.desktop.v2.VereinProfilV2(
"Verein-Profil #${currentScreen.id}", id = currentScreen.id,
onBack = onBack, onBack = onBack,
onAction = { onNavigate(AppScreen.VereinVerwaltung) },
actionLabel = "Zurück zur Zentrale"
) )
// --- Funktionaer-Verwaltung & Profil --- // --- Funktionaer-Verwaltung & Profil ---
@@ -412,24 +426,23 @@ private fun DesktopContentArea(
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) } onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) }
) )
is AppScreen.FunktionaerProfil -> PlaceholderScreen( is AppScreen.FunktionaerProfil -> at.mocode.desktop.v2.FunktionaerProfilV2(
"Funktionär-Profil #${currentScreen.id}", id = currentScreen.id,
onBack = onBack, onBack = onBack,
onAction = { onNavigate(AppScreen.FunktionaerVerwaltung) },
actionLabel = "Zurück zur Zentrale"
) )
// --- Veranstalter-Verwaltung & Profil --- // --- Veranstalter-Verwaltung & Profil ---
is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen( is AppScreen.VeranstalterVerwaltung -> at.mocode.desktop.v2.VeranstalterVerwaltungScreen(
onBack = onBack, onBack = onBack,
onNew = { onNavigate(AppScreen.VeranstalterNeu) },
onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) } onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) }
) )
is AppScreen.VeranstalterProfil -> PlaceholderScreen( is AppScreen.VeranstalterProfil -> at.mocode.desktop.v2.VeranstalterDetailV2(
"Veranstalter-Profil #${currentScreen.id}", veranstalterId = currentScreen.id,
onBack = onBack, onBack = onBack,
onAction = { onNavigate(AppScreen.PferdProfil(1L)) }, onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
actionLabel = "Pferde-Profil öffnen" onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(currentScreen.id)) },
) )
/* /*
@@ -445,9 +458,9 @@ private fun DesktopContentArea(
onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
) )
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen( is AppScreen.VeranstalterNeu -> at.mocode.desktop.v2.VeranstalterAnlegenWizard(
onAbbrechen = onBack, onCancel = onBack,
onSpeichern = { _, _, _ -> onBack() }, onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
) )
is AppScreen.VeranstalterDetail -> { is AppScreen.VeranstalterDetail -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
@@ -5,8 +5,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -35,7 +37,7 @@ fun <T> ManagementTableScreen(
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
} }
Text(title, style = MaterialTheme.typography.headlineMedium) Text(title, style = MaterialTheme.typography.headlineMedium)
} }
@@ -87,7 +89,7 @@ fun <T> ManagementTableScreen(
} }
} }
Divider() HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
// Table Body // Table Body
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
@@ -121,7 +123,7 @@ fun <T> ManagementTableScreen(
} }
} }
} }
Divider() HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
} }
} }
} }
@@ -248,7 +250,7 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
} }
@Composable @Composable
fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) {
// Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten
// Wir nutzen hier die 'vereine' Liste aus dem Store. // Wir nutzen hier die 'vereine' Liste aus dem Store.
val vereine = StoreV2.vereine val vereine = StoreV2.vereine
@@ -268,7 +270,7 @@ fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
TableColumn("Email", { it.email ?: "-" }, weight = 1f) TableColumn("Email", { it.email ?: "-" }, weight = 1f)
), ),
onBack = onBack, onBack = onBack,
onNew = { }, onNew = onNew,
onEdit = { onEdit(it.id) }, onEdit = { onEdit(it.id) },
onDelete = { }, onDelete = { },
onSearch = { filter = it } onSearch = { filter = it }
@@ -1,12 +1,14 @@
package at.mocode.desktop.v2 package at.mocode.desktop.v2
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -14,30 +16,66 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
@Composable @Composable
fun OnboardingScreen(onContinue: (String, String) -> Unit) { fun OnboardingScreen(
geraetName: String,
secureKey: String,
onGeraetNameChange: (String) -> Unit,
onSecureKeyChange: (String) -> Unit,
onContinue: (String, String) -> Unit,
) {
DesktopThemeV2 { DesktopThemeV2 {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Onboarding", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold) Text("Onboarding", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold)
var geraetName by remember { mutableStateOf("") }
var key by remember { mutableStateOf("") }
var showPw by remember { mutableStateOf(false) } var showPw by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
val frName = remember { FocusRequester() }
val frKey = remember { FocusRequester() }
val frBtn = remember { FocusRequester() }
OutlinedTextField( OutlinedTextField(
value = geraetName, value = geraetName,
onValueChange = { geraetName = it }, onValueChange = { onGeraetNameChange(it) },
label = { Text("Gerätename (Pflicht)") }, label = { Text("Gerätename (Pflicht)") },
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.focusRequester(frName)
.onKeyEvent { e ->
if (e.type == KeyEventType.KeyUp) {
when (e.key) {
Key.Tab, Key.Enter -> {
focusManager.moveFocus(FocusDirection.Next)
true
}
else -> false
}
} else false
}
,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
) )
OutlinedTextField( OutlinedTextField(
value = key, value = secureKey,
onValueChange = { key = it }, onValueChange = { onSecureKeyChange(it) },
label = { Text("Sicherheitsschlüssel (Pflicht)") }, label = { Text("Sicherheitsschlüssel (Pflicht)") },
trailingIcon = { trailingIcon = {
IconButton(onClick = { showPw = !showPw }) { IconButton(onClick = { showPw = !showPw }) {
@@ -45,12 +83,53 @@ fun OnboardingScreen(onContinue: (String, String) -> Unit) {
} }
}, },
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(), visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.focusRequester(frKey)
.onKeyEvent { e ->
if (e.type == KeyEventType.KeyUp) {
when (e.key) {
Key.Tab -> {
focusManager.moveFocus(FocusDirection.Next)
true
}
Key.Enter -> {
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
onContinue(geraetName, secureKey)
} else {
focusManager.moveFocus(FocusDirection.Next)
}
true
}
else -> false
}
} else false
}
,
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
onContinue(geraetName, secureKey)
} else {
focusManager.moveFocus(FocusDirection.Next)
}
})
) )
val enabled = geraetName.trim().length >= 3 && key.trim().length >= 8 val enabled = geraetName.trim().length >= 3 && secureKey.trim().length >= 8
Button(onClick = { onContinue(geraetName, key) }, enabled = enabled) { Button(
Text("Weiter zum VeranstalterFlow") onClick = { onContinue(geraetName, secureKey) },
enabled = enabled,
modifier = Modifier
.focusRequester(frBtn)
.onKeyEvent { e ->
if (e.type == KeyEventType.KeyUp && (e.key == Key.Enter)) {
if (enabled) onContinue(geraetName, secureKey)
true
} else false
}
) {
Text("Zu den Veranstaltungen")
} }
if (!enabled) Text("Mind. 3 Zeichen für Namen und 8 Zeichen für Schlüssel", color = Color(0xFFB00020)) if (!enabled) Text("Mind. 3 Zeichen für Namen und 8 Zeichen für Schlüssel", color = Color(0xFFB00020))
} }
@@ -58,6 +137,298 @@ fun OnboardingScreen(onContinue: (String, String) -> Unit) {
} }
} }
@Composable
fun PferdProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 {
val pferd = remember(id) { StoreV2.pferde.firstOrNull { it.id == id } }
if (pferd == null) { Text("Pferd nicht gefunden"); return@DesktopThemeV2 }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Pferde-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(56.dp).background(Color(0xFF374151), shape = MaterialTheme.shapes.small), contentAlignment = Alignment.Center) {
Text(pferd.name.take(2).uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(pferd.name, style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull(pferd.oepsNummer?.let { "OEPS: $it" }, pferd.feiId?.let { "FEI: $it" }).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
val l3 = listOfNotNull(pferd.geburtsdatum?.let { "geb. $it" }, pferd.farbe).joinToString(" · ")
if (l3.isNotBlank()) Text(l3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var name by remember { mutableStateOf(pferd.name) }
var oeps by remember { mutableStateOf(pferd.oepsNummer ?: "") }
var fei by remember { mutableStateOf(pferd.feiId ?: "") }
var geb by remember { mutableStateOf(pferd.geburtsdatum ?: "") }
var farbe by remember { mutableStateOf(pferd.farbe ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
pferd.name = name
pferd.oepsNummer = oeps.ifBlank { null }
pferd.feiId = fei.ifBlank { null }
pferd.geburtsdatum = geb.ifBlank { null }
pferd.farbe = farbe.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Pferd bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(geb, { geb = it }, label = { Text("Geburtsdatum") }, modifier = Modifier.weight(1f))
OutlinedTextField(farbe, { farbe = it }, label = { Text("Farbe") }, modifier = Modifier.weight(1f))
}
}
}
)
}
}
}
}
@Composable
fun ReiterProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 {
val r = remember(id) { StoreV2.reiter.firstOrNull { it.id == id } }
if (r == null) { Text("Reiter nicht gefunden"); return@DesktopThemeV2 }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Reiter-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(56.dp).background(Color(0xFF4B5563), shape = MaterialTheme.shapes.small), contentAlignment = Alignment.Center) {
val initials = (r.vorname + " " + r.nachname).trim().split(" ").mapNotNull { it.firstOrNull()?.toString() }.take(2).joinToString("")
Text(initials.uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("${r.vorname} ${r.nachname}", style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull(r.oepsNummer?.let { "OEPS: $it" }, r.feiId?.let { "FEI: $it" }, r.lizenzKlasse.takeIf { it.isNotBlank() } ).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
r.verein?.let { Text(it, color = Color(0xFF6B7280)) }
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var vor by remember { mutableStateOf(r.vorname) }
var nach by remember { mutableStateOf(r.nachname) }
var oeps by remember { mutableStateOf(r.oepsNummer ?: "") }
var fei by remember { mutableStateOf(r.feiId ?: "") }
var liz by remember { mutableStateOf(r.lizenzKlasse) }
var verein by remember { mutableStateOf(r.verein ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
r.vorname = vor
r.nachname = nach
r.oepsNummer = oeps.ifBlank { null }
r.feiId = fei.ifBlank { null }
r.lizenzKlasse = liz
r.verein = verein.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Reiter bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f))
OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(liz, { liz = it }, label = { Text("Lizenzklasse") }, modifier = Modifier.weight(1f))
OutlinedTextField(verein, { verein = it }, label = { Text("Verein") }, modifier = Modifier.weight(1f))
}
}
}
)
}
}
}
}
@Composable
fun VereinProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 {
val v = remember(id) { StoreV2.vereine.firstOrNull { it.id == id } }
if (v == null) { Text("Verein nicht gefunden"); return@DesktopThemeV2 }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Vereins-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small), contentAlignment = Alignment.Center) {
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(v.name, style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull("OEPS: ${v.oepsNummer}", v.ort, v.plz, v.strasse).filter { it.isNotBlank() }.joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
val l3 = listOfNotNull(v.email, v.telefon).filter { !it.isNullOrBlank() }.joinToString(" · ")
if (l3.isNotBlank()) Text(l3, color = Color(0xFF6B7280))
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var name by remember { mutableStateOf(v.name) }
var oeps by remember { mutableStateOf(v.oepsNummer) }
var ort by remember { mutableStateOf(v.ort ?: "") }
var plz by remember { mutableStateOf(v.plz ?: "") }
var strasse by remember { mutableStateOf(v.strasse ?: "") }
var email by remember { mutableStateOf(v.email ?: "") }
var tel by remember { mutableStateOf(v.telefon ?: "") }
var logo by remember { mutableStateOf(v.logoUrl ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
v.name = name
v.oepsNummer = oeps
v.ort = ort.ifBlank { null }
v.plz = plz.ifBlank { null }
v.strasse = strasse.ifBlank { null }
v.email = email.ifBlank { null }
v.telefon = tel.ifBlank { null }
v.logoUrl = logo.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Verein bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(oeps, { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
OutlinedTextField(logo, { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(ort, { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
OutlinedTextField(plz, { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
}
OutlinedTextField(strasse, { strasse = it }, label = { Text("Straße / Adresse") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
OutlinedTextField(tel, { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
}
}
}
)
}
}
}
}
@Composable
fun FunktionaerProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 {
val f = remember(id) { StoreV2.funktionaere.firstOrNull { it.id == id } }
if (f == null) { Text("Funktionär nicht gefunden"); return@DesktopThemeV2 }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") }
Text("Funktionärs-Profil", style = MaterialTheme.typography.titleLarge)
}
var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.size(56.dp).background(Color(0xFF111827), shape = MaterialTheme.shapes.small), contentAlignment = Alignment.Center) {
val initials = (f.vorname + " " + f.nachname).trim().split(" ").mapNotNull { it.firstOrNull()?.toString() }.take(2).joinToString("")
Text(initials.uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("${f.vorname} ${f.nachname}", style = MaterialTheme.typography.titleMedium)
val l2 = listOfNotNull(f.richterNummer?.let { "Nr. $it" }, f.richterQualifikation?.let { "Qual.: $it" }).joinToString(" · ")
if (l2.isNotBlank()) Text(l2, color = Color(0xFF6B7280))
f.email?.let { Text(it, color = Color(0xFF6B7280)) }
}
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
}
}
if (editOpen) {
var vor by remember { mutableStateOf(f.vorname) }
var nach by remember { mutableStateOf(f.nachname) }
var num by remember { mutableStateOf(f.richterNummer ?: "") }
var qual by remember { mutableStateOf(f.richterQualifikation ?: "") }
var email by remember { mutableStateOf(f.email ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
f.vorname = vor
f.nachname = nach
f.richterNummer = num.ifBlank { null }
f.richterQualifikation = qual.ifBlank { null }
f.email = email.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Funktionär bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f))
OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(num, { num = it }, label = { Text("Nummer") }, modifier = Modifier.weight(1f))
OutlinedTextField(qual, { qual = it }, label = { Text("Qualifikation") }, modifier = Modifier.weight(1f))
}
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.fillMaxWidth())
}
}
)
}
}
}
}
@Composable @Composable
fun VeranstalterAuswahlV2( fun VeranstalterAuswahlV2(
onBack: () -> Unit, onBack: () -> Unit,
@@ -125,62 +496,106 @@ fun VeranstalterDetailV2(
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") } Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
} }
// Profil-Bereich (Logo URL, Ansprechpartner, Kontakt, Adresse) // Veranstalter Vorschau-Karte mit Bearbeiten-Dialog
val verein = remember(veranstalterId) { StoreV2.vereine.firstOrNull { it.id == veranstalterId } } val verein = remember(veranstalterId) { StoreV2.vereine.firstOrNull { it.id == veranstalterId } }
if (verein != null) { if (verein != null) {
Card { var editOpen by remember { mutableStateOf(false) }
Column(Modifier.fillMaxWidth().padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Card(Modifier.fillMaxWidth()) {
Text("VeranstalterProfil", style = MaterialTheme.typography.titleMedium) Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField( // Logo/Avatar
value = verein.logoUrl ?: "", Box(
onValueChange = { verein.logoUrl = it.ifBlank { null } }, modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
label = { Text("LogoURL (optional)") }, contentAlignment = Alignment.Center
modifier = Modifier.fillMaxWidth() ) {
) Text((verein.kurzname ?: verein.name).take(2).uppercase(), color = Color.White, fontWeight = FontWeight.SemiBold)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.ort ?: "",
onValueChange = { verein.ort = it.ifBlank { null } },
label = { Text("Ansprechpartner / Ort (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = verein.telefon ?: "",
onValueChange = { verein.telefon = it.ifBlank { null } },
label = { Text("Telefon (optional)") },
modifier = Modifier.weight(1f)
)
} }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Spacer(Modifier.width(12.dp))
OutlinedTextField( Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
value = verein.email ?: "", Text(verein.name, style = MaterialTheme.typography.titleMedium)
onValueChange = { verein.email = it.ifBlank { null } }, val line2 = listOfNotNull("OEPS: ${verein.oepsNummer}", verein.ort, verein.plz, verein.strasse).filter { it.isNotBlank() }.joinToString(" · ")
label = { Text("EMail (optional)") }, if (line2.isNotBlank()) Text(line2, color = Color(0xFF6B7280))
modifier = Modifier.weight(1f) val line3 = listOfNotNull(verein.email, verein.telefon).filter { !it.isNullOrBlank() }.joinToString(" · ")
) if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280))
OutlinedTextField(
value = verein.oepsNummer,
onValueChange = { verein.oepsNummer = it },
label = { Text("OEPSNummer") },
modifier = Modifier.weight(1f)
)
} }
OutlinedTextField( Button(onClick = { editOpen = true }) { Text("bearbeiten") }
value = verein.strasse ?: "",
onValueChange = { verein.strasse = it.ifBlank { null } },
label = { Text("Adresse / Straße (optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 2
)
} }
} }
if (editOpen) {
// Lokale Edit-Felder
var name by remember { mutableStateOf(verein.name) }
var oeps by remember { mutableStateOf(verein.oepsNummer) }
var ort by remember { mutableStateOf(verein.ort ?: "") }
var plz by remember { mutableStateOf(verein.plz ?: "") }
var strasse by remember { mutableStateOf(verein.strasse ?: "") }
var email by remember { mutableStateOf(verein.email ?: "") }
var tel by remember { mutableStateOf(verein.telefon ?: "") }
var logo by remember { mutableStateOf(verein.logoUrl ?: "") }
AlertDialog(
onDismissRequest = { editOpen = false },
confirmButton = {
TextButton(onClick = {
// Speichern in Store
verein.name = name
verein.oepsNummer = oeps
verein.ort = ort.ifBlank { null }
verein.plz = plz.ifBlank { null }
verein.strasse = strasse.ifBlank { null }
verein.email = email.ifBlank { null }
verein.telefon = tel.ifBlank { null }
verein.logoUrl = logo.ifBlank { null }
editOpen = false
}) { Text("Speichern") }
},
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
title = { Text("Veranstalter bearbeiten") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = oeps, onValueChange = { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
OutlinedTextField(value = logo, onValueChange = { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = ort, onValueChange = { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
OutlinedTextField(value = plz, onValueChange = { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
}
OutlinedTextField(value = strasse, onValueChange = { strasse = it }, label = { Text("Straße / Adresse") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = email, onValueChange = { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
OutlinedTextField(value = tel, onValueChange = { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
}
}
}
)
}
} }
val events = StoreV2.eventsFor(veranstalterId) val events = StoreV2.eventsFor(veranstalterId)
if (events.isEmpty()) Text("Noch keine Veranstaltungen angelegt.", color = Color(0xFF6B7280)) // Filter-/Suchmaske
var search by remember { mutableStateOf("") }
OutlinedTextField(
value = search,
onValueChange = { search = it },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
placeholder = { Text("Veranstaltungen suchen…") },
modifier = Modifier.fillMaxWidth()
)
val filtered = remember(events, search) {
val q = search.trim()
if (q.isEmpty()) events else events.filter {
it.titel.contains(q, ignoreCase = true) ||
it.status.contains(q, ignoreCase = true) ||
it.datumVon.contains(q, ignoreCase = true) ||
(it.datumBis?.contains(q, ignoreCase = true) == true)
}
}
if (filtered.isEmpty()) Text("Keine passenden Veranstaltungen gefunden.", color = Color(0xFF6B7280))
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(events) { evt -> items(filtered) { evt ->
Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) { Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
@@ -417,7 +417,13 @@ fun VeranstaltungKonfigV2(
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
IconButton(onClick = { IconButton(onClick = {
if (currentStep > 1) { if (currentStep > 1) {
currentStep-- // Wenn wir aus einem konkreten Veranstalter kommen (id > 0),
// gehen wir bei Zurück direkt ins Profil statt auf Schritt 1.
if (veranstalterId != 0L) {
onBack()
} else {
currentStep--
}
} else { } else {
onBack() onBack()
} }
@@ -667,28 +673,60 @@ fun VeranstaltungKonfigV2(
Spacer(Modifier.width(1.dp)) Spacer(Modifier.width(1.dp))
} }
var showConfirm by remember { mutableStateOf(false) }
if (showConfirm) {
AlertDialog(
onDismissRequest = { showConfirm = false },
confirmButton = {
TextButton(onClick = {
val id = System.currentTimeMillis()
val v = VeranstaltungV2(
id = id,
veranstalterId = selectedVereinId,
titel = titel.trim(),
datumVon = von.trim(),
datumBis = bis.trim().ifBlank { null },
untertitel = untertitel.trim(),
ort = ort.trim().ifBlank { StoreV2.vereine.find { it.id == selectedVereinId }?.ort ?: "" },
logoUrl = logoUrl.trim().ifBlank { null }
)
sponsorenText.split(",").filter { it.isNotBlank() }.forEach { v.sponsoren.add(it.trim()) }
StoreV2.addEventFirst(selectedVereinId, v)
showConfirm = false
onSaved(id, selectedVereinId)
}) { Text("Anlegen") }
},
dismissButton = { TextButton(onClick = { showConfirm = false }) { Text("Abbrechen") } },
title = { Text("Veranstaltung final anlegen?") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Bitte die Daten prüfen. Für diese Veranstaltung wird eine eigene Datenbank initialisiert.")
HorizontalDivider()
val titelText = titel.trim()
val untertitelText = untertitel.trim().ifBlank { "-" }
val vonText = von.trim()
val bisText = bis.trim()
val zeitraumText = if (bisText.isNotEmpty()) "$vonText $bisText" else vonText
val vName = StoreV2.vereine.find { it.id == selectedVereinId }?.name ?: "#$selectedVereinId"
val spons = sponsorenText.split(',').map { it.trim() }.filter { it.isNotEmpty() }
Text("Titel: $titelText")
Text("Untertitel: $untertitelText")
Text("Zeitraum: $zeitraumText")
Text("Veranstalter: $vName")
if (logoUrl.isNotBlank()) Text("Logo: ${logoUrl.trim()}")
if (spons.isNotEmpty()) Text("Sponsoren: ${spons.joinToString(", ")}")
}
}
)
}
Button( Button(
onClick = { onClick = {
if (currentStep < 3) { if (currentStep < 3) {
currentStep++ currentStep++
} else { } else {
val id = System.currentTimeMillis() showConfirm = true
val v = VeranstaltungV2(
id = id,
veranstalterId = selectedVereinId,
titel = titel.trim(),
datumVon = von.trim(),
datumBis = bis.trim().ifBlank { null },
untertitel = untertitel.trim(),
ort = ort.trim().ifBlank { StoreV2.vereine.find { it.id == selectedVereinId }?.ort ?: "" },
logoUrl = logoUrl.trim().ifBlank { null }
)
sponsorenText.split(",").filter { it.isNotBlank() }.forEach {
v.sponsoren.add(it.trim())
}
StoreV2.addEventFirst(selectedVereinId, v)
onSaved(id, selectedVereinId)
} }
}, },
enabled = when (currentStep) { enabled = when (currentStep) {