docs: add event details and session log for Neumarkt 2026 tournaments
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 7m2s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m46s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 2m57s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m49s

- Added competition details for Neumarkt tournaments 26128 and 26129 under `docs/Neumarkt2026/`.
- Logged key outcomes of the Domain Workshop follow-up and Frontend Kick-off session under `docs/99_Journal/2026-03-18_Session_Log_Domain_und_Frontend_Kickoff.md`.
- Updated `frontend/shells/meldestelle-portal` with new routing and UI components for Landing Page, Dashboard, and Tournament creation flow.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-18 14:14:38 +01:00
parent 2538be395a
commit 32295bdea2
11 changed files with 810 additions and 93 deletions
@@ -3,6 +3,8 @@ package at.mocode.frontend.core.navigation
sealed class AppScreen(val route: String) {
data object Landing : AppScreen(Routes.HOME)
data object Home : AppScreen("/home")
data object Dashboard : AppScreen("/dashboard")
data object CreateTournament : AppScreen("/tournament/create") // Neuer Screen
data object Login : AppScreen(Routes.LOGIN)
data object Ping : AppScreen("/ping")
data object Profile : AppScreen("/profile")
@@ -13,6 +15,8 @@ sealed class AppScreen(val route: String) {
return when (route) {
Routes.HOME -> Landing
"/home" -> Home
"/dashboard" -> Dashboard
"/tournament/create" -> CreateTournament
Routes.LOGIN, Routes.Auth.LOGIN -> Login
"/ping" -> Ping
"/profile" -> Profile
@@ -1,19 +1,20 @@
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.collectAsState
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.auth.presentation.LoginScreen
import at.mocode.frontend.core.auth.presentation.LoginViewModel
import at.mocode.ping.feature.presentation.PingScreen
import at.mocode.ping.feature.presentation.PingViewModel
import at.mocode.frontend.core.designsystem.components.AppFooter
import at.mocode.frontend.core.designsystem.theme.AppTheme
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.ping.feature.presentation.PingScreen
import at.mocode.ping.feature.presentation.PingViewModel
import navigation.StateNavigationPort
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
@@ -39,31 +40,48 @@ fun MainApp() {
when (currentScreen) {
is AppScreen.Landing -> LandingScreen(
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login) },
onSecondary = { navigationPort.navigateToScreen(AppScreen.Home) }
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login) }
)
is AppScreen.Home -> WelcomeScreen(
is AppScreen.Dashboard -> DashboardScreen(
authTokenManager = authTokenManager,
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
onOpenLogin = { navigationPort.navigateToScreen(AppScreen.Login) },
onOpenProfile = { navigationPort.navigateToScreen(AppScreen.Profile) }
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) },
onLogout = {
authTokenManager.clearToken()
navigationPort.navigateToScreen(AppScreen.Landing)
}
)
is AppScreen.Home -> DashboardScreen( // Route /home to Dashboard for now
authTokenManager = authTokenManager,
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) },
onLogout = {
authTokenManager.clearToken()
navigationPort.navigateToScreen(AppScreen.Landing)
}
)
is AppScreen.CreateTournament -> CreateTournamentScreen(
onBack = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
onSave = { navigationPort.navigateToScreen(AppScreen.Dashboard) } // Later we go to tournament detail
)
is AppScreen.Login -> LoginScreen(
viewModel = loginViewModel,
onLoginSuccess = { navigationPort.navigateToScreen(AppScreen.Profile) },
onBack = { navigationPort.navigateToScreen(AppScreen.Home) }
onLoginSuccess = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
onBack = { navigationPort.navigateToScreen(AppScreen.Landing) }
)
is AppScreen.Ping -> PingScreen(
viewModel = pingViewModel,
onBack = { navigationPort.navigateToScreen(AppScreen.Home) } // Navigate back to Home
onBack = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
)
is AppScreen.Profile -> AuthStatusScreen(
authTokenManager = authTokenManager,
onBackToHome = { navigationPort.navigateToScreen(AppScreen.Home) }
onBackToHome = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
)
else -> {}
@@ -74,8 +92,7 @@ fun MainApp() {
@Composable
private fun LandingScreen(
onPrimaryCta: () -> Unit,
onSecondary: () -> Unit
onPrimaryCta: () -> Unit
) {
val scrollState = rememberScrollState()
@@ -84,56 +101,103 @@ private fun LandingScreen(
.fillMaxSize()
.verticalScroll(scrollState)
) {
// Top Bar area (simple for landing)
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "mo-code.at",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Button(onClick = onPrimaryCta) {
Text("Login Meldestelle")
}
}
}
// Hero
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "EquestEvents",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
text = "Die moderne Turniermeldestelle",
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "Von Praktikern für Praktiker. Schneller Nennen, fehlerfrei Richten und stressfrei Auswerten konform nach ÖTO & FEI.",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// --- AKTUELLE TURNIERE SECTION ---
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text("Aktuelle Turniere", style = MaterialTheme.typography.headlineMedium)
// Dummy Daten basierend auf Neumarkt 2026
val turniere = listOf(
TournamentData(
id = "26128",
date = "25. APRIL 2026",
title = "CSN-C NEU CSNP-C NEU",
location = "NEUMARKT/M., OÖ"
),
TournamentData(
id = "26129",
date = "26. APRIL 2026",
title = "CDN-C NEU CDNP-C NEU",
location = "NEUMARKT/M., OÖ"
)
)
Text(
text = "Die kompetente TurnierMeldestelle.",
style = MaterialTheme.typography.headlineLarge
)
Text(
text = "EquestEvents entwickelt die digitale Infrastruktur des österreichischen Pferdesports aus der Praxis. Für die Praxis.",
style = MaterialTheme.typography.bodyLarge
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onPrimaryCta) { Text("Anmelden (PilotPartner)") }
TextButton(onClick = onSecondary) { Text("Mehr erfahren") }
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
turniere.forEach { turnier ->
TournamentCard(turnier)
}
}
}
// Manifest
// Manifest / Intro
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Unser Anspruch: Durchdachtes System.", style = MaterialTheme.typography.headlineMedium)
Text("Unser Anspruch: Ein durchdachtes System.", style = MaterialTheme.typography.headlineMedium)
Text(
"Die Meldestelle ist das Herzstück jedes Turniers. Wenn sie stolpert, stockt der Sport. Wir verstehen den Balanceakt zwischen Veranstaltern, Reitern und den Verbänden.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Deshalb entwickeln wir EquestEvents nicht am Reißbrett, sondern direkt am Turnier aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.",
"Deshalb entwickeln wir diese Plattform nicht am Reißbrett, sondern direkt am Turnierplatz aus der Sicht der Meldestelle, der Richter, der Zeitnehmer und aller Funktionäre.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Aktuell befindet sich unser System in einer Pilotphase für C und CNEUTurniere. Wir wachsen organisch Seite an Seite mit unseren PilotPartnern.",
style = MaterialTheme.typography.bodyLarge
)
Text(
"Jedes Feedback fließt direkt in die Entwicklung ein, um eine Lösung zu schaffen, die den realen Bedürfnissen vor Ort entspricht.",
"Mit Fokus auf die Praxis: Tastaturbedienung für höchste Geschwindigkeit, Offline-Fähigkeit für das 'Plumpsklo' am Rand des Abreiteplatzes und eine integrierte Kassenführung.",
style = MaterialTheme.typography.bodyLarge
)
}
@@ -143,25 +207,30 @@ private fun LandingScreen(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 40.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
.padding(horizontal = 24.dp, vertical = 60.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
Text("Die drei Säulen", style = MaterialTheme.typography.headlineMedium)
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Die Kern-Säulen", style = MaterialTheme.typography.headlineMedium)
Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
FeatureCard(
number = "01",
title = "RegelwerksIntelligenz (FEI & ÖTO)",
body = "Wir verbinden ÖTO und FEI und nehmen Ihnen die ValidierungsKomplexität ab. Von der Lizenzprüfung bis zur korrekten Anwendung der Bestimmungen."
title = "Regelwerks-Intelligenz (ÖTO)",
body = "Wir nehmen Ihnen die Validierungs-Komplexität ab. Von der Lizenzprüfung der Reiter bis zur Kontrolle der Richterqualifikationen beim Anlegen der Bewerbe."
)
FeatureCard(
number = "02",
title = "Plattformunabhängig & Offlinefähig",
body = "Stabil auf Laptop und mobil. Dank OfflineUnterstützung arbeiten Sie nahtlos weiter selbst wenn die Internetverbindung am Platz abreißt."
title = "Offline-First & Resilient",
body = "Stabil auf dem Laptop. Dank Offline-Unterstützung und lokaler Datenbank arbeiten Sie nahtlos weiter, selbst wenn die Internetverbindung am Platz wieder einmal abreißt."
)
FeatureCard(
number = "03",
title = "Fokus auf den Sport",
body = "Wir reduzieren Administration dort, wo es sinnvoll ist damit sich alle auf das Wesentliche konzentrieren können: den Reitsport."
title = "Speed-Workflow",
body = "Die Nennungsmaske und die Ergebniserfassung sind kompromisslos auf Geschwindigkeit und Tastaturbedienung (Enter & Tab) optimiert. Weil am Turniertag jede Sekunde zählt."
)
FeatureCard(
number = "04",
title = "Smarte Kassenführung",
body = "Kontobasierte Abrechnung für Reiter und Besitzer. Nenngelder, Startgelder und Nachnenngebühren sauber getrennt selbst ein Nennungstausch wird als einfacher Transfer verbucht."
)
}
}
@@ -171,65 +240,513 @@ private fun LandingScreen(
}
}
// Data class for dummy tournament
private data class TournamentData(
val id: String,
val date: String,
val title: String,
val location: String
)
@Composable
private fun FeatureCard(number: String, title: String, body: String) {
Surface(tonalElevation = 0.dp) {
Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.width(56.dp).padding(top = 6.dp)) {
Text(text = number, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
private fun TournamentCard(data: TournamentData) {
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Left: Logo Placeholder
Surface(
modifier = Modifier.size(100.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
) {
Box(contentAlignment = Alignment.Center) {
Text(
"URFV\nLogo",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Column(modifier = Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(4.dp))
Text(body, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.width(24.dp))
// Middle: Info
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = data.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${data.location} ${data.date}",
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Turnier-Nr.:${data.id}",
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.width(24.dp))
// Right: Actions
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.width(200.dp)
) {
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Ausschreibung")
}
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Nennen")
}
OutlinedButton(
onClick = { /* TODO */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Start- Ergebnislisten")
}
}
}
}
}
@Composable
private fun WelcomeScreen(
private fun FeatureCard(number: String, title: String, body: String) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = number,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Black,
modifier = Modifier.width(64.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(body, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
@Composable
private fun DashboardScreen(
authTokenManager: AuthTokenManager,
onOpenPing: () -> Unit,
onOpenLogin: () -> Unit,
onOpenProfile: () -> Unit
onCreateTournament: () -> Unit,
onLogout: () -> Unit
) {
val authState by authTokenManager.authState.collectAsState()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Willkommen zur Meldestelle",
style = MaterialTheme.typography.headlineMedium
)
// App Header (Meldestelle Toolbar)
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Meldestelle Dashboard",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Angemeldet als: ${authState.username ?: "Admin"}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
TextButton(onClick = onLogout) {
Text("Abmelden")
}
}
}
}
// Auth info
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
if (authState.isAuthenticated) {
Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onOpenProfile) { Text("Profil anzeigen") }
} else {
Text("Du bist nicht angemeldet.")
// Main Content Area
Row(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
// Left Column (Turniere)
Column(
modifier = Modifier.weight(2f),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Meine Turniere", style = MaterialTheme.typography.headlineSmall)
// Dummy Turniere für die Meldestelle
val turniere = listOf(
TournamentData(
id = "26128",
date = "25. APRIL 2026",
title = "CSN-C NEU CSNP-C NEU",
location = "NEUMARKT/M., OÖ"
),
TournamentData(
id = "26129",
date = "26. APRIL 2026",
title = "CDN-C NEU CDNP-C NEU",
location = "NEUMARKT/M., OÖ"
)
)
turniere.forEach { turnier ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(turnier.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text("Nr: ${turnier.id} | ${turnier.date}", style = MaterialTheme.typography.bodyMedium)
}
Button(onClick = { /* TODO: Open Meldestellen Cockpit for this tournament */ }) {
Text("Meldestelle öffnen")
}
}
}
}
OutlinedButton(
onClick = onCreateTournament,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp)
) {
Text("+ Neues Turnier anlegen")
}
}
// Right Column (System / Tools)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("System & Tools", style = MaterialTheme.typography.headlineSmall)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(
onClick = onOpenPing,
modifier = Modifier.fillMaxWidth()
) {
Text("Ping-Service (System Status)")
}
OutlinedButton(
onClick = { /* TODO: ZNS Import */ },
modifier = Modifier.fillMaxWidth()
) {
Text("ZNS-Daten Importieren")
}
}
}
}
}
}
}
@Composable
fun CreateTournamentScreen(
onBack: () -> Unit,
onSave: () -> Unit
) {
// Simple state to track the current step in the wizard
var currentStep by remember { mutableStateOf(1) }
Column(modifier = Modifier.fillMaxSize()) {
// App Header
Surface(
color = MaterialTheme.colorScheme.surface,
shadowElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
TextButton(onClick = onBack) {
Text("← Zurück")
}
Text(
text = "Neues Turnier anlegen",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
}
}
// Actions
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = onOpenPing, modifier = Modifier.weight(1f)) { Text("Ping-Service") }
if (!authState.isAuthenticated) {
Button(
onClick = onOpenLogin,
modifier = Modifier.weight(1f)
) { Text("Login") }
// Stepper / Progress Bar
Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StepIndicator(step = 1, title = "Stammdaten", isActive = currentStep == 1, isCompleted = currentStep > 1)
StepIndicator(step = 2, title = "Konfiguration", isActive = currentStep == 2, isCompleted = currentStep > 2)
StepIndicator(step = 3, title = "Funktionäre", isActive = currentStep == 3, isCompleted = currentStep > 3)
StepIndicator(step = 4, title = "Bewerbe", isActive = currentStep == 4, isCompleted = currentStep > 4)
}
}
// Wizard Content Area
Box(modifier = Modifier.weight(1f).padding(24.dp)) {
when (currentStep) {
1 -> TournamentStepStammdaten()
2 -> TournamentStepKonfiguration()
3 -> TournamentStepFunktionaere()
4 -> TournamentStepBewerbe()
}
}
// Bottom Navigation Bar
Surface(shadowElevation = 8.dp, modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.padding(24.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
if (currentStep > 1) {
OutlinedButton(onClick = { currentStep-- }) { Text("Zurück") }
} else {
Spacer(modifier = Modifier.width(1.dp)) // Empty space to keep "Weiter" on the right
}
if (currentStep < 4) {
Button(onClick = { currentStep++ }) { Text("Weiter") }
} else {
Button(onClick = onSave) { Text("Turnier speichern") }
}
}
}
}
}
@Composable
fun StepIndicator(step: Int, title: String, isActive: Boolean, isCompleted: Boolean) {
val color = when {
isActive -> MaterialTheme.colorScheme.primary
isCompleted -> MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
}
val fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
shape = MaterialTheme.shapes.small,
color = color,
modifier = Modifier.size(24.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(step.toString(), color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.labelSmall)
}
}
Text(title, color = color, fontWeight = fontWeight)
}
}
@Composable
fun TournamentStepStammdaten() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 1: Turnier-Stammdaten", style = MaterialTheme.typography.headlineSmall)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turniernummer OEPS (z.B. 26128)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turniername (z.B. CSN-C NEU Neumarkt)") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Datum von") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Datum bis") },
modifier = Modifier.weight(1f)
)
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("ZNS Import", fontWeight = FontWeight.Bold)
Text(
"Hier laden wir später das ZNS.zip für dieses Turnier hoch, um Starter und Lizenzen zu importieren.",
style = MaterialTheme.typography.bodyMedium
)
Button(onClick = { /*TODO*/ }) { Text("ZNS.zip auswählen...") }
}
}
}
}
@Composable
fun TournamentStepKonfiguration() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 2: Konfiguration", style = MaterialTheme.typography.headlineSmall)
Text("Austragungsplätze und Preisliste")
// Placeholder for Austragungsplätze
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Austragungsplätze", fontWeight = FontWeight.Bold)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 8.dp)) {
FilterChip(selected = true, onClick = {}, label = { Text("Platz 1 (Sand)") })
FilterChip(selected = false, onClick = {}, label = { Text("Halle") })
FilterChip(selected = false, onClick = {}, label = { Text("+ Hinzufügen") })
}
}
}
}
}
@Composable
fun TournamentStepFunktionaere() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
Text("Schritt 3: Team & Funktionäre", style = MaterialTheme.typography.headlineSmall)
Text("Zuweisung von Richtern und Parcoursbauern (aus ZNS)")
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Turnierbeauftragter (Suche nach Name oder ID)") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Richter (Suche nach Name oder ID)") },
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
fun TournamentStepBewerbe() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxSize()) {
Text("Schritt 4: Bewerbe anlegen", style = MaterialTheme.typography.headlineSmall)
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
// Left: List of Bewerbe
Card(modifier = Modifier.weight(1f).fillMaxHeight()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Bewerbe", fontWeight = FontWeight.Bold)
TextButton(onClick = {}) { Text("+ Neu") }
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Text("1: Pony Stilspringprüfung (60cm)", modifier = Modifier.padding(vertical = 4.dp))
Text("2: Einlaufspringprüfung (60cm)", modifier = Modifier.padding(vertical = 4.dp))
Text("...", modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
// Right: Detail Tabs for selected Bewerb
Card(modifier = Modifier.weight(2f).fillMaxHeight()) {
Column {
// Tabs
PrimaryTabRow(selectedTabIndex = 0) {
Tab(selected = true, onClick = {}, text = { Text("Bewertung") })
Tab(selected = false, onClick = {}, text = { Text("Geldpreis") })
Tab(selected = false, onClick = {}, text = { Text("Ort/Zeit") })
}
// Tab Content (Bewertung)
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = "2",
onValueChange = {},
label = { Text("Bewerb Nr.") },
modifier = Modifier.width(100.dp)
)
OutlinedTextField(
value = "Einlaufspringprüfung",
onValueChange = {},
label = { Text("Bezeichnung") },
modifier = Modifier.weight(1f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedTextField(
value = "60cm",
onValueChange = {},
label = { Text("Klasse / Höhe") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = "§ 218",
onValueChange = {},
label = { Text("Richtverfahren") },
modifier = Modifier.weight(1f)
)
}
Text("Abteilungen", fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = true, onCheckedChange = {})
Text("1. Abt: lizenzfrei")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = true, onCheckedChange = {})
Text("2. Abt: mit Lizenz")
}
}
}
}
}
}
@@ -259,11 +776,11 @@ private fun AuthStatusScreen(
}) { Text("Abmelden") }
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = onBackToHome) { Text("Zurück zur Startseite") }
OutlinedButton(onClick = onBackToHome) { Text("Zurück zum Dashboard") }
} else {
Text("Nicht angemeldet.")
Spacer(Modifier.height(8.dp))
Button(onClick = onBackToHome) { Text("Zurück zur Startseite") }
Button(onClick = onBackToHome) { Text("Zurück") }
}
}
}