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:
parent
7a10d8bb18
commit
43d83e403a
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 Veranstalter‑Flow")
|
||||
}
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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("Turnier‑Nr. (5‑stellig)") }, 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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user