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:
+37
-55
@@ -293,27 +293,28 @@ private fun DesktopContentArea(
|
|||||||
// 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.screens.onboarding.OnboardingScreen(
|
// V2 Onboarding (Vision_03)
|
||||||
onContinue = { _, _, _ ->
|
at.mocode.desktop.v2.OnboardingScreenV2 { _, _ ->
|
||||||
// Dummy-Token setzen, damit nach Onboarding der Admin-Flow sichtbar ist
|
|
||||||
authTokenManager.setToken("dummy.jwt.token")
|
authTokenManager.setToken("dummy.jwt.token")
|
||||||
onNavigate(AppScreen.Veranstaltungen)
|
onNavigate(AppScreen.VeranstalterAuswahl)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root-Screen: Admin-Übersicht
|
// Root-Screen: Leitet in V2-Fluss
|
||||||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
is AppScreen.Veranstaltungen -> {
|
||||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
// Direkt zur Veranstalter-Auswahl V2
|
||||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) },
|
at.mocode.desktop.v2.VeranstalterAuswahlV2(
|
||||||
onPingService = { onNavigate(AppScreen.Ping) },
|
onBack = { /* bleibt root */ },
|
||||||
|
onWeiter = { vId -> onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
||||||
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlScreen(
|
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2(
|
||||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
||||||
@@ -328,74 +329,50 @@ private fun DesktopContentArea(
|
|||||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
VeranstalterDetailScreen(
|
at.mocode.desktop.v2.VeranstalterDetailV2(
|
||||||
veranstalterId = vId,
|
veranstalterId = vId,
|
||||||
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
onVeranstaltungOeffnen = { evtId ->
|
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) },
|
||||||
onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId))
|
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
|
||||||
},
|
|
||||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungKonfig -> {
|
is AppScreen.VeranstaltungKonfig -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
if (!FakeVeranstalterStore.exists(vId)) {
|
// V2: Validierung über StoreV2
|
||||||
|
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
at.mocode.veranstalter.feature.presentation.VeranstaltungKonfigScreen(
|
at.mocode.desktop.v2.VeranstaltungKonfigV2(
|
||||||
veranstalterId = vId,
|
veranstalterId = vId,
|
||||||
onAbbrechen = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
||||||
onSpeichern = { titel, datumVon, datumBis ->
|
onSaved = { evtId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId)) }
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungUebersicht -> {
|
is AppScreen.VeranstaltungUebersicht -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
val evtId = currentScreen.veranstaltungId
|
val evtId = currentScreen.veranstaltungId
|
||||||
if (!FakeVeranstalterStore.exists(vId)) {
|
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||||
)
|
)
|
||||||
} else if (!FakeVeranstaltungStore.belongsTo(vId, evtId)) {
|
} else if (at.mocode.desktop.v2.StoreV2.eventsFor(vId).none { it.id == evtId }) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
|
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
|
||||||
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
|
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
VeranstaltungUebersichtScreen(
|
at.mocode.desktop.v2.VeranstaltungUebersichtV2(
|
||||||
veranstalterId = vId,
|
veranstalterId = vId,
|
||||||
veranstaltungId = evtId,
|
veranstaltungId = evtId,
|
||||||
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
||||||
onTurnierOeffnen = { tId ->
|
|
||||||
onNavigate(AppScreen.TurnierDetail(evtId, tId))
|
|
||||||
},
|
|
||||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) },
|
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) },
|
||||||
onZnsImport = { /* TODO */ },
|
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
|
||||||
onDbImport = { /* TODO */ },
|
|
||||||
onDbExport = { /* TODO */ },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,16 +407,21 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
is AppScreen.TurnierNeu -> {
|
is AppScreen.TurnierNeu -> {
|
||||||
val evtId = currentScreen.veranstaltungId
|
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(
|
InvalidContextNotice(
|
||||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
||||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TurnierNeuScreen(
|
at.mocode.desktop.v2.TurnierWizardV2(
|
||||||
|
veranstalterId = parent.id,
|
||||||
veranstaltungId = evtId,
|
veranstaltungId = evtId,
|
||||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
onSaved = { _ -> onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+159
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+198
@@ -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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user