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
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:
+4
@@ -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 = "Equest‑Events",
|
||||
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 Turnier‑Meldestelle.",
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
Text(
|
||||
text = "Equest‑Events 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 (Pilot‑Partner)") }
|
||||
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 Equest‑Events 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 C‑NEU‑Turniere. Wir wachsen organisch – Seite an Seite mit unseren Pilot‑Partnern.",
|
||||
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 = "Regelwerks‑Intelligenz (FEI & ÖTO)",
|
||||
body = "Wir verbinden ÖTO und FEI – und nehmen Ihnen die Validierungs‑Komplexitä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 & Offline‑fähig",
|
||||
body = "Stabil auf Laptop und mobil. Dank Offline‑Unterstü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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user