Implement Vision_03: Overhaul Veranstalter and Veranstaltung flow with new composables, centralized state management via StoreV2, and updated navigation logic. Add DesktopThemeV2 for consistent UI styling.

This commit is contained in:
Stefan Mogeritsch 2026-03-28 02:08:14 +01:00
parent 7a10d8bb18
commit 43d83e403a
5 changed files with 474 additions and 58 deletions

View File

@ -293,27 +293,28 @@ private fun DesktopContentArea(
// Onboarding ohne Login
is AppScreen.Onboarding -> {
val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject()
at.mocode.desktop.screens.onboarding.OnboardingScreen(
onContinue = { _, _, _ ->
// Dummy-Token setzen, damit nach Onboarding der Admin-Flow sichtbar ist
authTokenManager.setToken("dummy.jwt.token")
onNavigate(AppScreen.Veranstaltungen)
}
// V2 Onboarding (Vision_03)
at.mocode.desktop.v2.OnboardingScreenV2 { _, _ ->
authTokenManager.setToken("dummy.jwt.token")
onNavigate(AppScreen.VeranstalterAuswahl)
}
}
// Root-Screen: Leitet in V2-Fluss
is AppScreen.Veranstaltungen -> {
// Direkt zur Veranstalter-Auswahl V2
at.mocode.desktop.v2.VeranstalterAuswahlV2(
onBack = { /* bleibt root */ },
onWeiter = { vId -> onNavigate(AppScreen.VeranstalterDetail(vId)) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
)
}
// Root-Screen: Admin-Übersicht
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
onPingService = { onNavigate(AppScreen.Ping) },
)
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2(
onBack = { onNavigate(AppScreen.Veranstaltungen) },
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
)
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
@ -328,74 +329,50 @@ private fun DesktopContentArea(
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
)
} else {
VeranstalterDetailScreen(
at.mocode.desktop.v2.VeranstalterDetailV2(
veranstalterId = vId,
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
onVeranstaltungOeffnen = { evtId ->
onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId))
},
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) },
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) },
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
)
}
}
is AppScreen.VeranstaltungKonfig -> {
val vId = currentScreen.veranstalterId
if (!FakeVeranstalterStore.exists(vId)) {
// V2: Validierung über StoreV2
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
)
} else {
at.mocode.veranstalter.feature.presentation.VeranstaltungKonfigScreen(
at.mocode.desktop.v2.VeranstaltungKonfigV2(
veranstalterId = vId,
onAbbrechen = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onSpeichern = { titel, datumVon, datumBis ->
// ID generieren und in den Fake-Store einfügen
val id = System.currentTimeMillis()
at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore.addFirst(
vId,
at.mocode.veranstalter.feature.presentation.VeranstaltungListUiModel(
id = id,
name = titel,
datum = if (datumBis.isNotBlank()) "$datumVon $datumBis" else datumVon,
ort = "",
turnierAnzahl = 1,
nennungen = 0,
bewerbe = 0,
letzteAktivitaet = "soeben",
status = at.mocode.frontend.core.designsystem.models.VeranstaltungStatus.VORBEREITUNG,
)
)
onNavigate(AppScreen.VeranstalterDetail(vId))
}
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onSaved = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) }
)
}
}
is AppScreen.VeranstaltungUebersicht -> {
val vId = currentScreen.veranstalterId
val evtId = currentScreen.veranstaltungId
if (!FakeVeranstalterStore.exists(vId)) {
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
)
} else if (!FakeVeranstaltungStore.belongsTo(vId, evtId)) {
} else if (at.mocode.desktop.v2.StoreV2.eventsFor(vId).none { it.id == evtId }) {
InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
)
} else {
VeranstaltungUebersichtScreen(
at.mocode.desktop.v2.VeranstaltungUebersichtV2(
veranstalterId = vId,
veranstaltungId = evtId,
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onTurnierOeffnen = { tId ->
onNavigate(AppScreen.TurnierDetail(evtId, tId))
},
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) },
onZnsImport = { /* TODO */ },
onDbImport = { /* TODO */ },
onDbExport = { /* TODO */ },
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
)
}
}
@ -430,16 +407,21 @@ private fun DesktopContentArea(
}
is AppScreen.TurnierNeu -> {
val evtId = currentScreen.veranstaltungId
if (!FakeVeranstaltungStore.exists(evtId)) {
// V2: wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert
val parent = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { v ->
at.mocode.desktop.v2.StoreV2.eventsFor(v.id).any { it.id == evtId }
}
if (parent == null) {
InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
onBack = { onNavigate(AppScreen.Veranstaltungen) }
)
} else {
TurnierNeuScreen(
at.mocode.desktop.v2.TurnierWizardV2(
veranstalterId = parent.id,
veranstaltungId = evtId,
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
onSave = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
)
}
}

View File

@ -0,0 +1,159 @@
package at.mocode.desktop.v2
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@Composable
fun OnboardingScreenV2(onContinue: (String, String) -> Unit) {
DesktopThemeV2 {
Surface(color = MaterialTheme.colorScheme.background) {
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
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) }
OutlinedTextField(
value = geraetName,
onValueChange = { geraetName = it },
label = { Text("Gerätename (Pflicht)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = key,
onValueChange = { key = it },
label = { Text("Sicherheitsschlüssel (Pflicht)") },
trailingIcon = {
IconButton(onClick = { showPw = !showPw }) {
Icon(if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = null)
}
},
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
val enabled = geraetName.trim().length >= 3 && key.trim().length >= 8
Button(onClick = { onContinue(geraetName, key) }, enabled = enabled) {
Text("Weiter zum VeranstalterFlow")
}
if (!enabled) Text("Mind. 3 Zeichen für Namen und 8 Zeichen für Schlüssel", color = Color(0xFFB00020))
}
}
}
}
@Composable
fun VeranstalterAuswahlV2(
onBack: () -> Unit,
onWeiter: (Long) -> Unit,
onNeu: () -> Unit,
) {
DesktopThemeV2 {
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück", modifier = Modifier.clickable { onBack() })
Text("Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f))
OutlinedButton(onClick = onNeu) { Text("+ Neuer Veranstalter") }
}
var selectedId by remember { mutableStateOf<Long?>(null) }
LazyColumn(Modifier.fillMaxSize()) {
items(StoreV2.vereine) { v ->
val sel = selectedId == v.id
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clickable {
selectedId = v.id
// Direktnavigation beim Klick auf eine Karte
onWeiter(v.id)
},
border = if (sel) ButtonDefaults.outlinedButtonBorder else null,
) {
Column(Modifier.padding(12.dp)) {
Text(v.name, fontWeight = FontWeight.SemiBold)
Text("OEPS: ${v.oepsNummer} · ${v.ort}", color = Color(0xFF6B7280))
}
}
}
}
Button(onClick = { selectedId?.let(onWeiter) }, enabled = selectedId != null) { Text("Weiter zum Veranstalter") }
}
}
}
@Composable
fun VeranstalterDetailV2(
veranstalterId: Long,
onBack: () -> Unit,
onZurVeranstaltung: (Long) -> Unit,
onNeuVeranstaltung: () -> Unit,
) {
DesktopThemeV2 {
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück", modifier = Modifier.clickable { onBack() })
val verein = StoreV2.vereine.firstOrNull { it.id == veranstalterId }
Text(verein?.name ?: "Veranstalter", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f))
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
}
val events = StoreV2.eventsFor(veranstalterId)
if (events.isEmpty()) Text("Noch keine Veranstaltungen angelegt.", color = Color(0xFF6B7280))
LazyColumn(Modifier.fillMaxSize()) {
items(events) { evt ->
Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text(evt.titel, fontWeight = FontWeight.SemiBold)
Text("${evt.datumVon}${evt.datumBis?.let { " $it" } ?: ""}", color = Color(0xFF6B7280))
AssistChip(onClick = {}, label = { Text(evt.status) })
}
Button(onClick = { onZurVeranstaltung(evt.id) }) { Text("Zur Veranstaltung") }
Spacer(Modifier.width(8.dp))
var confirm by remember { mutableStateOf(false) }
if (confirm) {
AlertDialog(
onDismissRequest = { confirm = false },
confirmButton = {
TextButton(onClick = {
StoreV2.removeEvent(veranstalterId, evt.id)
confirm = false
}) { Text("Löschen") }
},
dismissButton = { TextButton(onClick = { confirm = false }) { Text("Abbrechen") } },
title = { Text("Veranstaltung löschen?") },
text = { Text("Diese Aktion entfernt die Veranstaltung und alle zugehörigen Turniere im Prototypen.") }
)
}
IconButton(onClick = { confirm = true }) { Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626)) }
}
}
}
}
}
}
}

View File

@ -0,0 +1,42 @@
package at.mocode.desktop.v2
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
data class Verein(
val id: Long,
val name: String,
val oepsNummer: String,
val ort: String,
)
data class VeranstaltungV2(
val id: Long,
val veranstalterId: Long,
var titel: String,
var datumVon: String,
var datumBis: String?,
var status: String = "In Vorbereitung",
)
object StoreV2 {
val vereine: SnapshotStateList<Verein> = mutableStateListOf(
Verein(1, "Union Reit- und Fahrverein Neumarkt/M.", "V-OOE-0001", "Neumarkt/M."),
Verein(2, "Pferdesportverein Linz", "V-OOE-0002", "Linz"),
)
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf()
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> =
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
fun addEventFirst(vereinId: Long, v: VeranstaltungV2) {
eventsFor(vereinId).add(0, v)
}
fun removeEvent(vereinId: Long, veranstaltungId: Long) {
val list = eventsFor(vereinId)
val idx = list.indexOfFirst { it.id == veranstaltungId }
if (idx >= 0) list.removeAt(idx)
}
}

View File

@ -0,0 +1,35 @@
package at.mocode.desktop.v2
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
// Vision_03 Farbschema (vereinfacht)
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
private val Surface = Color(0xFFFFFFFF)
private val Background = Color(0xFFF7F8FA)
private val LightColors: ColorScheme = lightColorScheme(
primary = PrimaryBlue,
secondary = AccentBlue,
background = Background,
surface = Surface,
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color(0xFF0F172A),
onSurface = Color(0xFF111827),
)
@Composable
fun DesktopThemeV2(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = LightColors,
typography = Typography(),
content = content,
)
}

View File

@ -0,0 +1,198 @@
package at.mocode.desktop.v2
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun VeranstaltungKonfigV2(
veranstalterId: Long,
onBack: () -> Unit,
onSaved: (Long) -> Unit,
) {
DesktopThemeV2 {
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück", modifier = Modifier.clickable { onBack() })
Text("Neue Veranstaltung", style = MaterialTheme.typography.titleLarge)
}
var titel by remember { mutableStateOf("") }
var von by remember { mutableStateOf("") }
var bis by remember { mutableStateOf("") }
OutlinedTextField(value = titel, onValueChange = { titel = it }, label = { Text("Titel (Pflicht)") }, modifier = Modifier.fillMaxWidth())
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = von, onValueChange = { von = it }, label = { Text("von (YYYY-MM-DD)") }, modifier = Modifier.weight(1f))
OutlinedTextField(value = bis, onValueChange = { bis = it }, label = { Text("bis (YYYY-MM-DD)") }, modifier = Modifier.weight(1f))
}
val validDates = von.isNotBlank() && (bis.isBlank() || bis >= von)
if (!validDates) Text("bis-Datum darf nicht vor von-Datum liegen", color = Color(0xFFB00020))
val enabled = titel.trim().isNotEmpty() && validDates
Button(onClick = {
val id = System.currentTimeMillis()
StoreV2.addEventFirst(veranstalterId, VeranstaltungV2(id, veranstalterId, titel.trim(), von.trim(), bis.trim().ifBlank { null }))
onSaved(id)
}, enabled = enabled) { Text("Speichern") }
}
}
}
data class TurnierV2(
val id: Long,
val veranstaltungId: Long,
val turnierNr: Int,
var kategorie: String,
var datumVon: String,
var datumBis: String?,
)
object TurnierStoreV2 {
private val map = mutableMapOf<Long, MutableList<TurnierV2>>()
fun list(veranstaltungId: Long): MutableList<TurnierV2> = map.getOrPut(veranstaltungId) { mutableListOf() }
fun add(veranstaltungId: Long, t: TurnierV2) { list(veranstaltungId).add(0, t) }
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
}
@Composable
fun VeranstaltungUebersichtV2(
veranstalterId: Long,
veranstaltungId: Long,
onBack: () -> Unit,
onTurnierNeu: () -> Unit,
onTurnierOpen: (Long) -> Unit,
) {
DesktopThemeV2 {
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück", modifier = Modifier.clickable { onBack() })
Text(veranstaltung?.titel ?: "Veranstaltung", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f))
Button(onClick = onTurnierNeu) { Text("+ Neues Turnier") }
}
if (veranstaltung != null) {
Text("Zeitraum: ${veranstaltung.datumVon}${veranstaltung.datumBis?.let { " $it" } ?: ""}")
Text("Status: ${veranstaltung.status}")
}
val list = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) }
if (list.isEmpty()) Text("Noch keine Turniere angelegt.", color = Color(0xFF6B7280))
LazyColumn(Modifier.fillMaxSize()) {
items(list) { t ->
Card(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text("Turnier ${t.turnierNr}", fontWeight = FontWeight.SemiBold)
Text(t.kategorie, color = Color(0xFF6B7280))
Text("${t.datumVon}${t.datumBis?.let { " $it" } ?: ""}", color = Color(0xFF6B7280))
}
Button(onClick = { onTurnierOpen(t.id) }) { Text("Zum Turnier") }
Spacer(Modifier.width(8.dp))
var confirm by remember { mutableStateOf(false) }
if (confirm) {
AlertDialog(
onDismissRequest = { confirm = false },
confirmButton = { TextButton(onClick = { TurnierStoreV2.remove(veranstaltungId, t.id); confirm = false }) { Text("Löschen") } },
dismissButton = { TextButton(onClick = { confirm = false }) { Text("Abbrechen") } },
title = { Text("Turnier löschen?") },
text = { Text("Dieses Turnier wird aus der Veranstaltung entfernt (Prototyp).") }
)
}
IconButton(onClick = { confirm = true }) { Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626)) }
}
}
}
}
}
}
}
@Composable
fun TurnierWizardV2(
veranstalterId: Long,
veranstaltungId: Long,
onBack: () -> Unit,
onSaved: (Long) -> Unit,
) {
DesktopThemeV2 {
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.ArrowBack, contentDescription = "Zurück", modifier = Modifier.clickable { onBack() })
Text("Neues Turnier", style = MaterialTheme.typography.titleLarge)
}
var nr by remember { mutableStateOf("") }
var locked by remember { mutableStateOf(false) }
// Kategorie wird gemäß Neumarkt-Logik automatisch vorbelegt aus der Turnier-Nr.
var kat by remember { mutableStateOf("") }
var von by remember { mutableStateOf(veranstaltung?.datumVon ?: "") }
var bis by remember { mutableStateOf(veranstaltung?.datumBis ?: "") }
OutlinedTextField(value = nr, onValueChange = {
if (!locked) nr = it.filter { ch -> ch.isDigit() }.take(5)
}, label = { Text("TurnierNr. (5stellig)") }, enabled = !locked, modifier = Modifier.fillMaxWidth())
val nrValid = nr.length == 5
Button(onClick = {
// Auto-Mapping gemäß vorhandener Neumarkt-Dokumentation
kat = when (nr) {
"26128" -> "CSN-C-NEU CSNP-C-NEU"
"26129" -> "CDN-C-NEU CDNP-C-NEU"
else -> ""
}
locked = true
}, enabled = nrValid && !locked) { Text("Nummer bestätigen & initialisieren") }
if (!nrValid) Text("Genau 5 Ziffern erforderlich", color = Color(0xFFB00020))
val freigeschaltet = locked
// Kategorie-Auswahl gemäß Vorlage: Dropdown mit sinnvollen Optionen und Auto-Vorbelegung
val kategorien = remember { listOf("CDN-C-NEU CDNP-C-NEU", "CSN-C-NEU CSNP-C-NEU") }
var katMenu by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Kategorie:")
Box {
OutlinedButton(onClick = { if (freigeschaltet) katMenu = true }, enabled = freigeschaltet) {
Text(if (kat.isBlank()) "Kategorie wählen" else kat)
}
DropdownMenu(expanded = katMenu, onDismissRequest = { katMenu = false }) {
kategorien.forEach { k ->
DropdownMenuItem(onClick = { kat = k; katMenu = false }, text = { Text(k) })
}
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = von, onValueChange = { von = it }, label = { Text("von (YYYY-MM-DD)") }, enabled = freigeschaltet, modifier = Modifier.weight(1f))
OutlinedTextField(value = bis, onValueChange = { bis = it }, label = { Text("bis (YYYY-MM-DD)") }, enabled = freigeschaltet, modifier = Modifier.weight(1f))
}
val parentVon = veranstaltung?.datumVon
val parentBis = veranstaltung?.datumBis
val dateOk = freigeschaltet && von.isNotBlank() && (bis.isBlank() || bis >= von) &&
(parentVon == null || (von >= parentVon && (parentBis == null || (bis.isBlank() || bis <= parentBis))))
if (freigeschaltet && !dateOk) Text("Turnier-Datum muss im Veranstaltungszeitraum liegen", color = Color(0xFFB00020))
Button(onClick = {
val id = System.currentTimeMillis()
TurnierStoreV2.add(veranstaltungId, TurnierV2(id, veranstaltungId, nr.toInt(), kat, von, bis.ifBlank { null }))
onSaved(id)
}, enabled = freigeschaltet && nrValid && kat.isNotBlank() && dateOk) { Text("Speichern") }
}
}
}