feat(event-feature): enhance Veranstaltungs- & Turnier-Workflow
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run
- Extended `Veranstaltung` domain model with new fields: `untertitel`, `logoUrl`, and `sponsoren`. - Refined navigation in `DesktopMainLayout.kt` to check turnier context and improve routing. - Overhauled `TurnierStammdatenTab` with enhanced interactivity: dynamic chip-based selectors for Spartens, Klassen, and Sponsors, as well as date pickers and ZNS import handling. - Implemented validations for date ranges and required fields. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+8
-6
@@ -23,9 +23,7 @@ import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||
import at.mocode.ping.feature.presentation.PingScreen
|
||||
import at.mocode.ping.feature.presentation.PingViewModel
|
||||
import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||
import at.mocode.turnier.feature.presentation.TurnierWizardV2
|
||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
@@ -388,7 +386,10 @@ private fun DesktopContentArea(
|
||||
// Turnier-Screens
|
||||
is AppScreen.TurnierDetail -> {
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
if (!FakeVeranstaltungStore.exists(evtId)) {
|
||||
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) }
|
||||
@@ -397,7 +398,7 @@ private fun DesktopContentArea(
|
||||
TurnierDetailScreen(
|
||||
veranstaltungId = evtId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -413,10 +414,11 @@ private fun DesktopContentArea(
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||
)
|
||||
} else {
|
||||
TurnierWizardV2(
|
||||
at.mocode.desktop.v2.TurnierWizardV2(
|
||||
veranstalterId = parent.id,
|
||||
veranstaltungId = evtId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||
onSave = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||
onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -65,14 +65,19 @@ object StoreV2 {
|
||||
)
|
||||
)
|
||||
|
||||
// Turniere für Neumarkt
|
||||
TurnierStoreV2.add(
|
||||
neumarktId,
|
||||
TurnierV2(101, neumarktId, 26128, "CSN-C-NEU CSNP-C-NEU", "2026-04-10", "2026-04-12")
|
||||
TurnierV2(101, neumarktId, 26128, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
||||
kategorie.add("CSN-C-NEU")
|
||||
kategorie.add("CSNP-C-NEU")
|
||||
}
|
||||
)
|
||||
TurnierStoreV2.add(
|
||||
neumarktId,
|
||||
TurnierV2(102, neumarktId, 26129, "CDN-C-NEU CDNP-C-NEU", "2026-04-10", "2026-04-12")
|
||||
TurnierV2(102, neumarktId, 26129, datumVon = "2026-04-10", datumBis = "2026-04-12", znsDataLoaded = true).apply {
|
||||
kategorie.add("CDN-C-NEU")
|
||||
kategorie.add("CDNP-C-NEU")
|
||||
}
|
||||
)
|
||||
|
||||
// 2. Linz 2026 (ID 200)
|
||||
@@ -88,7 +93,11 @@ object StoreV2 {
|
||||
beschreibung = "Großes Reit-Event am Ebelsberger Schlosspark."
|
||||
)
|
||||
)
|
||||
TurnierStoreV2.add(linzId, TurnierV2(201, linzId, 26500, "CSN-B*", "2026-05-20", "2026-05-24"))
|
||||
TurnierStoreV2.add(
|
||||
linzId,
|
||||
TurnierV2(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply {
|
||||
kategorie.add("CSN-B*")
|
||||
})
|
||||
|
||||
// 3. Ein historisches Event (ID 300)
|
||||
addEventFirst(
|
||||
|
||||
+763
-93
@@ -4,17 +4,23 @@ 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.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
@@ -497,7 +503,10 @@ fun VeranstaltungKonfigV2(
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val isDateRangeInvalid = dateVon != null && dateBis != null && dateBis.isBefore(dateVon)
|
||||
val today = LocalDate.now()
|
||||
val isStartInPast = dateVon != null && dateVon.isBefore(today)
|
||||
val isDateRangeInvalid =
|
||||
(dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
@@ -506,10 +515,11 @@ fun VeranstaltungKonfigV2(
|
||||
label = { Text("Datum von") },
|
||||
modifier = Modifier.weight(1f).clickable { showDatePickerVon = true },
|
||||
enabled = false,
|
||||
isError = isStartInPast,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledBorderColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
@@ -517,6 +527,11 @@ fun VeranstaltungKonfigV2(
|
||||
IconButton(onClick = { showDatePickerVon = true }) {
|
||||
Icon(Icons.Default.DateRange, contentDescription = "Datum wählen")
|
||||
}
|
||||
},
|
||||
supportingText = {
|
||||
if (isStartInPast) {
|
||||
Text("Startdatum darf nicht in der Vergangenheit liegen.")
|
||||
}
|
||||
}
|
||||
)
|
||||
OutlinedTextField(
|
||||
@@ -643,7 +658,9 @@ fun VeranstaltungKonfigV2(
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val rangeInvalid = dVon != null && dBis != null && dBis.isBefore(dVon)
|
||||
val today2 = LocalDate.now()
|
||||
val startInPast = dVon != null && dVon.isBefore(today2)
|
||||
val rangeInvalid = (dVon != null && dBis != null && dBis.isBefore(dVon)) || startInPast
|
||||
titel.isNotBlank() && von.isNotBlank() && !rangeInvalid
|
||||
}
|
||||
|
||||
@@ -662,9 +679,16 @@ data class TurnierV2(
|
||||
val id: Long,
|
||||
val veranstaltungId: Long,
|
||||
val turnierNr: Int,
|
||||
var kategorie: String,
|
||||
var typ: String = "ÖTO (National)",
|
||||
var znsDataLoaded: Boolean = false,
|
||||
var sparten: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var klassen: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var kategorie: SnapshotStateList<String> = mutableStateListOf(),
|
||||
var datumVon: String,
|
||||
var datumBis: String?,
|
||||
var titel: String = "",
|
||||
var subTitel: String = "",
|
||||
var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
|
||||
)
|
||||
|
||||
object TurnierStoreV2 {
|
||||
@@ -684,48 +708,100 @@ fun VeranstaltungUebersichtV2(
|
||||
) {
|
||||
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.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text(veranstaltung?.titel ?: "Veranstaltung", style = MaterialTheme.typography.titleLarge)
|
||||
val turniere = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) }
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = veranstaltung?.titel ?: "Veranstaltung",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
if (veranstaltung != null) {
|
||||
Text(
|
||||
text = "${veranstaltung.ort} | ${veranstaltung.datumVon}${veranstaltung.datumBis?.let { " – $it" } ?: ""}",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = onTurnierNeu) { Text("+ Neues Turnier") }
|
||||
ElevatedButton(
|
||||
onClick = onTurnierNeu,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Neues Turnier")
|
||||
}
|
||||
}
|
||||
|
||||
if (veranstaltung != null) {
|
||||
Text("Zeitraum: ${veranstaltung.datumVon}${veranstaltung.datumBis?.let { " – $it" } ?: ""}")
|
||||
Text("Status: ${veranstaltung.status}")
|
||||
// KPI Dashboard
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
KpiCard(
|
||||
title = "Turniere",
|
||||
value = turniere.size.toString(),
|
||||
icon = Icons.Default.Event,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
KpiCard(
|
||||
title = "Nennungen",
|
||||
value = if (veranstaltungId == 100L) "248" else "0",
|
||||
icon = Icons.Default.Description,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
KpiCard(
|
||||
title = "Reiter",
|
||||
value = if (veranstaltungId == 100L) "112" else "0",
|
||||
icon = Icons.Default.Person,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
KpiCard(
|
||||
title = "Pferde",
|
||||
value = if (veranstaltungId == 100L) "145" else "0",
|
||||
icon = Icons.Default.Pets,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
val list = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) }
|
||||
if (list.isEmpty()) Text("Noch keine Turniere angelegt.", color = Color(0xFF6B7280))
|
||||
// Turnierliste
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Zugeordnete Turniere", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||
if (turniere.isNotEmpty()) {
|
||||
Badge(containerColor = MaterialTheme.colorScheme.secondaryContainer) {
|
||||
Text(turniere.size.toString(), color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)) }
|
||||
}
|
||||
if (turniere.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
"Noch keine Turniere angelegt.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(bottom = 24.dp)
|
||||
) {
|
||||
items(turniere) { t ->
|
||||
TurnierCard(
|
||||
turnier = t,
|
||||
onOpen = { onTurnierOpen(t.id) },
|
||||
onDelete = { TurnierStoreV2.remove(veranstaltungId, t.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,6 +809,109 @@ fun VeranstaltungUebersichtV2(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpiCard(
|
||||
title: String,
|
||||
value: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = androidx.compose.foundation.shape.CircleShape,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(title, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnierCard(
|
||||
turnier: TurnierV2,
|
||||
onOpen: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onOpen
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
"Turnier #${turnier.turnierNr}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
turnier.kategorie.forEach { kat ->
|
||||
SuggestionChip(
|
||||
onClick = {},
|
||||
label = { Text(kat, fontSize = 11.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${turnier.datumVon}${turnier.datumBis?.let { " – $it" } ?: ""}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = onOpen) {
|
||||
Text("Öffnen")
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = { showDeleteConfirm = true }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirm = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDelete()
|
||||
showDeleteConfirm = false
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
|
||||
) { Text("Löschen") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteConfirm = false }) { Text("Abbrechen") }
|
||||
},
|
||||
title = { Text("Turnier löschen?") },
|
||||
text = { Text("Möchten Sie das Turnier #${turnier.turnierNr} wirklich löschen?") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TurnierWizardV2(
|
||||
veranstalterId: Long,
|
||||
@@ -742,70 +921,561 @@ fun TurnierWizardV2(
|
||||
) {
|
||||
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.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text("Neues Turnier", style = MaterialTheme.typography.titleLarge)
|
||||
var currentStep by remember { mutableStateOf(1) }
|
||||
var showZnsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// State für alle Felder
|
||||
var nr by remember { mutableStateOf("") }
|
||||
var nrConfirmed by remember { mutableStateOf(false) }
|
||||
var znsDataLoaded by remember { mutableStateOf(false) }
|
||||
var typ by remember { mutableStateOf("ÖTO (National)") }
|
||||
|
||||
val sparten = remember { mutableStateListOf<String>() }
|
||||
val klassen = remember { mutableStateListOf<String>() }
|
||||
val kat = remember { mutableStateListOf<String>() }
|
||||
var von by remember { mutableStateOf(veranstaltung?.datumVon ?: "") }
|
||||
var bis by remember { mutableStateOf(veranstaltung?.datumBis ?: "") }
|
||||
|
||||
var titel by remember { mutableStateOf("") }
|
||||
var subTitel by remember { mutableStateOf("") }
|
||||
val sponsoren = remember { mutableStateListOf<String>() }
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
// Header mit Breadcrumbs-Optik
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
"Schritt $currentStep von 3",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
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 ?: "") }
|
||||
LinearProgressIndicator(
|
||||
progress = { currentStep / 3f },
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp).clip(RoundedCornerShape(4.dp)),
|
||||
)
|
||||
|
||||
OutlinedTextField(value = nr, onValueChange = {
|
||||
if (!locked) nr = it.filter { ch -> ch.isDigit() }.take(5)
|
||||
}, label = { Text("Turnier‑Nr. (5‑stellig)") }, enabled = !locked, modifier = Modifier.fillMaxWidth())
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
when (currentStep) {
|
||||
1 -> Step1Basics(
|
||||
nr, { nr = it },
|
||||
nrConfirmed, { nrConfirmed = it },
|
||||
typ, { typ = it },
|
||||
znsDataLoaded, { znsDataLoaded = it }
|
||||
)
|
||||
|
||||
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 -> ""
|
||||
2 -> Step2Sparten(
|
||||
sparten, klassen, kat,
|
||||
von, { von = it }, bis, { bis = it },
|
||||
veranstaltung
|
||||
)
|
||||
|
||||
3 -> Step3Branding(titel, { titel = it }, subTitel, { subTitel = it }, sponsoren)
|
||||
}
|
||||
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)
|
||||
// Footer Navigation
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OutlinedButton(
|
||||
onClick = { if (currentStep > 1) currentStep-- else onBack() }
|
||||
) {
|
||||
Text(if (currentStep == 1) "Abbrechen" else "Zurück")
|
||||
}
|
||||
|
||||
val canContinue = when (currentStep) {
|
||||
1 -> nr.length == 5 && nrConfirmed && znsDataLoaded
|
||||
2 -> {
|
||||
val vVon = veranstaltung?.datumVon?.let { LocalDate.parse(it) }
|
||||
val vBis = veranstaltung?.datumBis?.let { LocalDate.parse(it) }
|
||||
val tVon = try {
|
||||
LocalDate.parse(von)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val tBis = if (bis.isBlank()) tVon else try {
|
||||
LocalDate.parse(bis)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val dateValid = if (vVon != null && tVon != null) {
|
||||
val startOk = !tVon.isBefore(vVon)
|
||||
val endOk = if (vBis != null && tBis != null) !tBis.isAfter(vBis) && !tBis.isBefore(tVon) else true
|
||||
startOk && endOk
|
||||
} else true
|
||||
|
||||
sparten.isNotEmpty() && klassen.isNotEmpty() && kat.isNotEmpty() && von.isNotBlank() && dateValid
|
||||
}
|
||||
DropdownMenu(expanded = katMenu, onDismissRequest = { katMenu = false }) {
|
||||
kategorien.forEach { k ->
|
||||
DropdownMenuItem(onClick = { kat = k; katMenu = false }, text = { Text(k) })
|
||||
|
||||
3 -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (currentStep < 3) {
|
||||
if (currentStep == 1) {
|
||||
// Auto-Mapping bei Schritt-Wechsel
|
||||
if (kat.isEmpty()) {
|
||||
if (nr == "26128") {
|
||||
if (!kat.contains("CSN-C-NEU")) kat.add("CSN-C-NEU")
|
||||
if (!kat.contains("CSNP-C-NEU")) kat.add("CSNP-C-NEU")
|
||||
}
|
||||
if (nr == "26129") {
|
||||
if (!kat.contains("CDN-C-NEU")) kat.add("CDN-C-NEU")
|
||||
if (!kat.contains("CDNP-C-NEU")) kat.add("CDNP-C-NEU")
|
||||
}
|
||||
}
|
||||
}
|
||||
currentStep++
|
||||
} else {
|
||||
val id = System.currentTimeMillis()
|
||||
val newTurnier = TurnierV2(
|
||||
id = id,
|
||||
veranstaltungId = veranstaltungId,
|
||||
turnierNr = nr.toInt(),
|
||||
typ = typ,
|
||||
znsDataLoaded = znsDataLoaded,
|
||||
datumVon = von,
|
||||
datumBis = bis.ifBlank { null },
|
||||
titel = titel,
|
||||
subTitel = subTitel
|
||||
)
|
||||
newTurnier.sparten.addAll(sparten)
|
||||
newTurnier.klassen.addAll(klassen)
|
||||
newTurnier.kategorie.addAll(kat)
|
||||
newTurnier.sponsoren.addAll(sponsoren)
|
||||
|
||||
TurnierStoreV2.add(veranstaltungId, newTurnier)
|
||||
onSaved(id)
|
||||
}
|
||||
},
|
||||
enabled = canContinue
|
||||
) {
|
||||
Text(if (currentStep == 3) "Turnier erstellen" else "Weiter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step1Basics(
|
||||
nr: String, onNrChange: (String) -> Unit,
|
||||
nrConfirmed: Boolean, onNrConfirmedChange: (Boolean) -> Unit,
|
||||
typ: String, onTypChange: (String) -> Unit,
|
||||
znsDataLoaded: Boolean, onZnsDataLoadedChange: (Boolean) -> Unit
|
||||
) {
|
||||
var showImportProgress by remember { mutableStateOf(false) }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Turnier-Konfiguration", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = nr,
|
||||
onValueChange = {
|
||||
onNrChange(it.filter { ch -> ch.isDigit() }.take(5))
|
||||
onNrConfirmedChange(false)
|
||||
},
|
||||
label = { Text("Turnier-Nr. (z.B. 26128)") },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !nrConfirmed,
|
||||
supportingText = { Text("5-stellige Nummer vom OePS") }
|
||||
)
|
||||
|
||||
if (!nrConfirmed) {
|
||||
Button(
|
||||
onClick = { onNrConfirmedChange(true) },
|
||||
enabled = nr.length == 5
|
||||
) {
|
||||
Text("Bestätigen")
|
||||
}
|
||||
} else {
|
||||
Icon(Icons.Default.CheckCircle, "Bestätigt", tint = Color(0xFF4CAF50), modifier = Modifier.size(32.dp))
|
||||
TextButton(onClick = { onNrConfirmedChange(false) }) { Text("Ändern") }
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("Typ:", style = MaterialTheme.typography.labelLarge)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
listOf("ÖTO (National)", "FEI (International)").forEach { option ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { onTypChange(option) }) {
|
||||
RadioButton(selected = typ == option, onClick = { onTypChange(option) })
|
||||
Text(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
if (typ.startsWith("ÖTO")) "Nationales Regelwerk kommt zum Einsatz" else "Internationales FEI-Reglement aktiv",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("ZNS-Daten:", style = MaterialTheme.typography.labelLarge)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = { showImportProgress = true },
|
||||
enabled = nrConfirmed && !znsDataLoaded
|
||||
) {
|
||||
Icon(Icons.Default.CloudDownload, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Import via Internet")
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { showImportProgress = true },
|
||||
enabled = nrConfirmed && !znsDataLoaded
|
||||
) {
|
||||
Icon(Icons.Default.Usb, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Import via USB")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Surface(
|
||||
color = if (znsDataLoaded) Color(0xFFE8F5E9) else Color(0xFFFFEBEE),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (znsDataLoaded) Icons.Default.CheckCircle else Icons.Default.Error,
|
||||
null,
|
||||
tint = if (znsDataLoaded) Color(0xFF2E7D32) else Color(0xFFC62828)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
if (znsDataLoaded) "ZNS-Daten geladen" else "Keine ZNS-Daten (Import erforderlich)",
|
||||
color = if (znsDataLoaded) Color(0xFF2E7D32) else Color(0xFFC62828),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showImportProgress) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
confirmButton = { },
|
||||
title = { Text("ZNS Import") },
|
||||
text = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Daten werden verarbeitet...")
|
||||
Spacer(Modifier.height(16.dp))
|
||||
CircularProgressIndicator()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(2000)
|
||||
onZnsDataLoadedChange(true)
|
||||
showImportProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun Step2Sparten(
|
||||
sparten: SnapshotStateList<String>,
|
||||
klassen: SnapshotStateList<String>,
|
||||
kat: SnapshotStateList<String>,
|
||||
von: String, onVonChange: (String) -> Unit,
|
||||
bis: String, onBisChange: (String) -> Unit,
|
||||
veranstaltung: VeranstaltungV2?
|
||||
) {
|
||||
var showDatePickerVon by remember { mutableStateOf(false) }
|
||||
var showDatePickerBis by remember { mutableStateOf(false) }
|
||||
|
||||
val vVon = veranstaltung?.datumVon?.let { LocalDate.parse(it) }
|
||||
val vBis = veranstaltung?.datumBis?.let { LocalDate.parse(it) }
|
||||
val tVon = try {
|
||||
LocalDate.parse(von)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val tBis = if (bis.isBlank()) tVon else try {
|
||||
LocalDate.parse(bis)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val isDateValid = if (vVon != null && tVon != null) {
|
||||
val startOk = !tVon.isBefore(vVon)
|
||||
val endOk = if (vBis != null && tBis != null) !tBis.isAfter(vBis) && !tBis.isBefore(tVon) else true
|
||||
startOk && endOk
|
||||
} else true
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Sparten & Klassen", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
// Datumsauswahl
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
"Zeitraum des Turniers:",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = von,
|
||||
onValueChange = {},
|
||||
label = { Text("Datum von") },
|
||||
modifier = Modifier.weight(1f),
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showDatePickerVon = true }) {
|
||||
Icon(Icons.Default.DateRange, contentDescription = "Kalender")
|
||||
}
|
||||
},
|
||||
isError = !isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon),
|
||||
supportingText = {
|
||||
if (!isDateValid && tVon != null && vVon != null && tVon.isBefore(vVon)) {
|
||||
Text("Muss innerhalb der Veranstaltung liegen (${veranstaltung?.datumVon})")
|
||||
}
|
||||
}
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = bis,
|
||||
onValueChange = {},
|
||||
label = { Text("Datum bis (optional)") },
|
||||
modifier = Modifier.weight(1f),
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showDatePickerBis = true }) {
|
||||
Icon(Icons.Default.DateRange, contentDescription = "Kalender")
|
||||
}
|
||||
},
|
||||
isError = !isDateValid && tBis != null && ((vBis != null && tBis.isAfter(vBis)) || (tVon != null && tBis.isBefore(
|
||||
tVon
|
||||
))),
|
||||
supportingText = {
|
||||
if (!isDateValid && tBis != null) {
|
||||
if (vBis != null && tBis.isAfter(vBis)) Text("Darf nicht nach der Veranstaltung enden (${veranstaltung?.datumBis})")
|
||||
else if (tVon != null && tBis.isBefore(tVon)) Text("Darf nicht vor dem Startdatum liegen")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDatePickerVon) {
|
||||
val datePickerState = rememberDatePickerState(
|
||||
initialSelectedDateMillis = tVon?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||
?: System.currentTimeMillis()
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDatePickerVon = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
datePickerState.selectedDateMillis?.let {
|
||||
onVonChange(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate().toString())
|
||||
}
|
||||
showDatePickerVon = false
|
||||
}) { Text("OK") }
|
||||
}
|
||||
) { DatePicker(state = datePickerState) }
|
||||
}
|
||||
|
||||
if (showDatePickerBis) {
|
||||
val datePickerState = rememberDatePickerState(
|
||||
initialSelectedDateMillis = tBis?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||
?: tVon?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||
?: System.currentTimeMillis()
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDatePickerBis = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
datePickerState.selectedDateMillis?.let {
|
||||
onBisChange(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate().toString())
|
||||
}
|
||||
showDatePickerBis = false
|
||||
}) { Text("OK") }
|
||||
}
|
||||
) { DatePicker(state = datePickerState) }
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("Sparten:", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
listOf("Dressur", "Springen").forEach { option ->
|
||||
FilterChip(
|
||||
selected = sparten.contains(option),
|
||||
onClick = { if (sparten.contains(option)) sparten.remove(option) else sparten.add(option) },
|
||||
label = { Text(option) },
|
||||
leadingIcon = if (sparten.contains(option)) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("Klassen:", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
val allKlassen = listOf("C-NEU", "C", "B", "A", "L", "LM", "M", "S")
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
allKlassen.chunked(4).forEach { rowKlassen ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
rowKlassen.forEach { option ->
|
||||
FilterChip(
|
||||
selected = klassen.contains(option),
|
||||
onClick = { if (klassen.contains(option)) klassen.remove(option) else klassen.add(option) },
|
||||
label = { Text(option, modifier = Modifier.padding(horizontal = 8.dp)) },
|
||||
leadingIcon = if (klassen.contains(option)) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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") }
|
||||
Column {
|
||||
Text(
|
||||
"Kategorie (Vorschläge):",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
val suggestions = remember(sparten.toList(), klassen.toList()) {
|
||||
val list = mutableListOf<String>()
|
||||
sparten.forEach { s ->
|
||||
val prefix = if (s == "Dressur") "CDN" else "CSN"
|
||||
|
||||
klassen.forEach { k ->
|
||||
val suffix = if (k == "C-NEU") "-C-NEU" else "-$k"
|
||||
list.add("$prefix$suffix")
|
||||
list.add("${prefix}P$suffix")
|
||||
}
|
||||
}
|
||||
list.distinct()
|
||||
}
|
||||
|
||||
if (sparten.isEmpty() || klassen.isEmpty()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().height(60.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text("Bitte Sparte(n) und Klasse(n) auswählen", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
suggestions.chunked(4).forEach { chunk ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
chunk.forEach { suggestion ->
|
||||
InputChip(
|
||||
selected = kat.contains(suggestion),
|
||||
onClick = { if (kat.contains(suggestion)) kat.remove(suggestion) else kat.add(suggestion) },
|
||||
label = { Text(suggestion) },
|
||||
trailingIcon = if (kat.contains(suggestion)) {
|
||||
{ Icon(Icons.Default.Check, null, modifier = Modifier.size(18.dp)) }
|
||||
} else null,
|
||||
colors = InputChipDefaults.inputChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step3Branding(
|
||||
titel: String, onTitelChange: (String) -> Unit,
|
||||
subTitel: String, onSubTitelChange: (String) -> Unit,
|
||||
sponsoren: SnapshotStateList<String>
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Turnier-Beschreibung", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
OutlinedTextField(
|
||||
value = titel,
|
||||
onValueChange = onTitelChange,
|
||||
label = { Text("Titel (z.B. Frühjahrs-Turnier 2026)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = subTitel,
|
||||
onValueChange = onSubTitelChange,
|
||||
label = { Text("Sub-Titel (z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Sponsoren", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(onClick = { sponsoren.add("") }) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
Text("Sponsor hinzufügen")
|
||||
}
|
||||
}
|
||||
|
||||
if (sponsoren.isEmpty()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text("Noch keine Sponsoren hinzugefügt", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sponsoren.forEachIndexed { index, sponsor ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = sponsor,
|
||||
onValueChange = { sponsoren[index] = it },
|
||||
label = { Text("Sponsor Name") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { sponsoren.removeAt(index) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user