All checks were successful
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 8m4s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 7m14s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 3m17s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m51s
1585 lines
59 KiB
Kotlin
1585 lines
59 KiB
Kotlin
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.graphics.Color
|
||
import androidx.compose.ui.text.font.FontWeight
|
||
import androidx.compose.ui.text.style.TextAlign
|
||
import androidx.compose.ui.unit.dp
|
||
import androidx.compose.material.icons.Icons
|
||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||
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.frontend.core.designsystem.components.AppFooter
|
||
import at.mocode.frontend.core.designsystem.theme.AppTheme
|
||
import at.mocode.frontend.core.domain.PlatformType
|
||
import at.mocode.frontend.core.domain.currentPlatform
|
||
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
|
||
|
||
@Composable
|
||
fun MainApp() {
|
||
// Wrap the entire app in our centralized AppTheme
|
||
AppTheme {
|
||
Surface(
|
||
modifier = Modifier.fillMaxSize(),
|
||
color = MaterialTheme.colorScheme.background
|
||
) {
|
||
// Resolve NavigationPort to observe state changes
|
||
val navigationPort = koinInject<StateNavigationPort>()
|
||
val currentScreen by navigationPort.currentScreen.collectAsState()
|
||
|
||
// Resolve AuthTokenManager from Koin
|
||
val authTokenManager = koinInject<AuthTokenManager>()
|
||
|
||
// Delta-Sync blueprint: resolve the Ping feature view model via Koin.
|
||
val pingViewModel: PingViewModel = koinViewModel()
|
||
val loginViewModel: LoginViewModel = koinViewModel()
|
||
|
||
when (val screen = currentScreen) {
|
||
is AppScreen.Landing -> {
|
||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||
// Master Desktop: MUSS eingeloggt sein, um irgendwas zu sehen.
|
||
// Wir leiten für den POC erst mal hart auf den Login, wenn nicht eingeloggt
|
||
val authState = authTokenManager.authState.collectAsState().value
|
||
if (authState.isAuthenticated) {
|
||
DashboardScreen(
|
||
authTokenManager = authTokenManager,
|
||
onLogout = {
|
||
authTokenManager.clearToken()
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard))
|
||
},
|
||
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) }
|
||
)
|
||
} else {
|
||
LaunchedEffect(Unit) {
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard))
|
||
}
|
||
}
|
||
} else {
|
||
LandingScreen(
|
||
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard)) }, // Takes you to Meldestelle login
|
||
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.OrganizerProfile)) } // Nach Login zum OrganizerProfile
|
||
)
|
||
}
|
||
}
|
||
|
||
is AppScreen.Dashboard -> DashboardScreen(
|
||
authTokenManager = authTokenManager,
|
||
onLogout = {
|
||
authTokenManager.clearToken()
|
||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard))
|
||
} else {
|
||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||
}
|
||
},
|
||
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) }
|
||
)
|
||
|
||
is AppScreen.Home -> DashboardScreen( // Route /home to Dashboard for now
|
||
authTokenManager = authTokenManager,
|
||
onLogout = {
|
||
authTokenManager.clearToken()
|
||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard))
|
||
} else {
|
||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||
}
|
||
},
|
||
onCreateTournament = { navigationPort.navigateToScreen(AppScreen.CreateTournament) }
|
||
)
|
||
|
||
is AppScreen.CreateTournament -> CreateTournamentScreen(
|
||
onBack = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
|
||
onSave = { navigationPort.navigateToScreen(AppScreen.Dashboard) } // Later we go to tournament detail
|
||
)
|
||
|
||
is AppScreen.Login -> {
|
||
// wir prüfen hier, woher wir kamen (falls wir das im State hätten)
|
||
// Im Moment: nach erfolgreichem Login zum returnTo-Screen navigieren, Default ist Dashboard
|
||
LoginScreen(
|
||
viewModel = loginViewModel,
|
||
onLoginSuccess = {
|
||
val targetScreen = screen.returnTo ?: AppScreen.Dashboard
|
||
navigationPort.navigateToScreen(targetScreen)
|
||
},
|
||
onBack = {
|
||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||
// No real back from login in desktop if forced, but let's go to Dashboard empty state
|
||
// This is a bit tricky, but for PoC we just clear
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Dashboard))
|
||
} else {
|
||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
is AppScreen.Ping -> PingScreen(
|
||
viewModel = pingViewModel,
|
||
onBack = { navigationPort.navigateToScreen(AppScreen.Profile) } // Zurück zum Profil/Übersicht
|
||
)
|
||
|
||
is AppScreen.OrganizerProfile -> OrganizerProfileScreen(
|
||
authTokenManager = authTokenManager,
|
||
onLogout = {
|
||
authTokenManager.clearToken()
|
||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||
},
|
||
onNavigateToDashboard = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
|
||
)
|
||
|
||
is AppScreen.AuthCallback -> {
|
||
// OIDC-Callback: Nach erfolgreichem OAuth-Redirect zum Dashboard navigieren
|
||
LaunchedEffect(Unit) {
|
||
navigationPort.navigateToScreen(AppScreen.Dashboard)
|
||
}
|
||
}
|
||
|
||
is AppScreen.Profile -> AuthStatusScreen(
|
||
authTokenManager = authTokenManager,
|
||
onNavigateToLogin = {
|
||
// Hier nutzen wir den dynamischen Return-Pfad: Nach dem Login soll der User
|
||
// exakt auf dieses AuthStatusScreen/Profile zurückkehren.
|
||
navigationPort.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Profile))
|
||
},
|
||
onNavigateToPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
|
||
onBackToHome = {
|
||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||
navigationPort.navigateToScreen(AppScreen.Dashboard)
|
||
} else {
|
||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun LandingScreen(
|
||
onPrimaryCta: () -> Unit,
|
||
onOpenPing: () -> Unit
|
||
) {
|
||
val scrollState = rememberScrollState()
|
||
|
||
Column(
|
||
modifier = Modifier
|
||
.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 = 60.dp),
|
||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
Text(
|
||
text = "Die moderne Meldestelle",
|
||
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
|
||
)
|
||
}
|
||
|
||
// Manifest / Intro
|
||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 24.dp, vertical = 60.dp),
|
||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||
) {
|
||
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 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(
|
||
"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
|
||
)
|
||
}
|
||
}
|
||
|
||
// Features
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 24.dp, vertical = 60.dp),
|
||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||
) {
|
||
Text("Die Kern-Säulen", style = MaterialTheme.typography.headlineMedium)
|
||
Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||
FeatureCard(
|
||
number = "01",
|
||
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 = "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 = "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."
|
||
)
|
||
}
|
||
}
|
||
|
||
// Ping Service Link
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.padding(horizontal = 24.dp, vertical = 24.dp),
|
||
horizontalAlignment = Alignment.CenterHorizontally
|
||
) {
|
||
OutlinedButton(onClick = onOpenPing) {
|
||
Text("System Status (Ping-Service)")
|
||
}
|
||
}
|
||
|
||
// Footer
|
||
AppFooter()
|
||
}
|
||
}
|
||
|
||
// Data class for dummy tournament
|
||
private data class TournamentData(
|
||
val id: String,
|
||
val date: String,
|
||
val title: String,
|
||
val location: String
|
||
)
|
||
|
||
@Composable
|
||
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
|
||
)
|
||
}
|
||
}
|
||
|
||
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 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,
|
||
onLogout: () -> Unit,
|
||
onCreateTournament: () -> Unit
|
||
) {
|
||
val authState by authTokenManager.authState.collectAsState()
|
||
val scrollState = rememberScrollState()
|
||
val isDesktop = currentPlatform() == PlatformType.DESKTOP
|
||
|
||
// Security Check für das Dashboard
|
||
if (!authState.isAuthenticated) {
|
||
// Wenn nicht eingeloggt, zeige nur eine leere Seite oder einen Hinweis an
|
||
// (Die Umleitung zum Login passiert in MainApp bzw. LaunchedEffect)
|
||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||
CircularProgressIndicator()
|
||
}
|
||
return
|
||
}
|
||
|
||
val isAdmin = authTokenManager.isAdmin()
|
||
|
||
Column(
|
||
modifier = Modifier.fillMaxSize()
|
||
) {
|
||
// 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 = if (isDesktop) "Master-Meldestelle Steuerungszentrale" else if (isAdmin) "Admin-Dashboard (Web)" else "Veranstalter-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")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Main Content Area
|
||
Column(
|
||
modifier = Modifier.fillMaxSize().padding(24.dp).verticalScroll(scrollState),
|
||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||
) {
|
||
|
||
if (isDesktop && isAdmin) {
|
||
// DESKTOP VIEW - STEUERUNGSZENTRALE FÜR DEN ADMIN (DICH)
|
||
// Neues Turnier anlegen Button
|
||
OutlinedButton(
|
||
onClick = onCreateTournament,
|
||
modifier = Modifier.fillMaxWidth().height(64.dp)
|
||
) {
|
||
Text(
|
||
text = "+ neues Turnier anlegen",
|
||
style = MaterialTheme.typography.titleMedium
|
||
)
|
||
}
|
||
|
||
// Meine Turniere Section
|
||
Text(
|
||
text = "Alle verwalteten Turniere",
|
||
style = MaterialTheme.typography.headlineSmall,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
|
||
// Filters (Mockup)
|
||
Surface(
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||
shape = MaterialTheme.shapes.small,
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
Text("Zeitraum:", style = MaterialTheme.typography.bodyMedium)
|
||
OutlinedTextField(
|
||
value = "März",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(100.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
Text("bis", style = MaterialTheme.typography.bodyMedium)
|
||
OutlinedTextField(
|
||
value = "Dezember",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(120.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
OutlinedTextField(
|
||
value = "2026",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(80.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
OutlinedTextField(
|
||
value = "Bundesland",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(150.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
Button(
|
||
onClick = {},
|
||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE67E22))
|
||
) { // Orange color
|
||
Text("Anzeigen")
|
||
}
|
||
}
|
||
Text("Zusätzliche Filter auf Suchergebnisse:", style = MaterialTheme.typography.bodyMedium)
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
OutlinedTextField(
|
||
value = "Veranstalter (Verein)",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(180.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
OutlinedTextField(
|
||
value = "Ort",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(120.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
OutlinedTextField(
|
||
value = "Sparte",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(120.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
OutlinedTextField(
|
||
value = "Turnierart",
|
||
onValueChange = {},
|
||
modifier = Modifier.width(120.dp).height(48.dp),
|
||
singleLine = true
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Desktop Tournament Card (Steuerungszentrale Ansicht)
|
||
Card(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
border = androidx.compose.foundation.BorderStroke(2.dp, MaterialTheme.colorScheme.outlineVariant),
|
||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||
) {
|
||
Row(
|
||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
horizontalArrangement = Arrangement.SpaceBetween
|
||
) {
|
||
// Left Side: Logo and Text
|
||
Row(
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||
modifier = Modifier.weight(1f)
|
||
) {
|
||
// Logo Placeholder
|
||
Surface(
|
||
modifier = Modifier.size(120.dp),
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||
shape = MaterialTheme.shapes.small
|
||
) {
|
||
Box(contentAlignment = Alignment.Center) {
|
||
Text("NEUMARKT\nLOGO", textAlign = TextAlign.Center, style = MaterialTheme.typography.bodySmall)
|
||
}
|
||
}
|
||
|
||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||
Text("CDN-C NEU CDNP-C", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||
Text(
|
||
"Veranstalter: URFV Neumarkt",
|
||
style = MaterialTheme.typography.bodyMedium,
|
||
color = MaterialTheme.colorScheme.primary
|
||
)
|
||
Text("NEUMARKT/M., OÖ 26. APRIL 2026", style = MaterialTheme.typography.bodyMedium)
|
||
Text("Turnier-Nr.: 26129", style = MaterialTheme.typography.bodyMedium)
|
||
}
|
||
}
|
||
|
||
// Right Side: Toggles (Statusanzeigen für den Admin)
|
||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
|
||
ToggleRow("Meldestelle-Desktop online", isOnline = true, isInteractive = false)
|
||
ToggleRow("Nennsystem online", isOnline = true, isInteractive = true)
|
||
ToggleRow("Start- Ergebnislisten online", isOnline = true, isInteractive = true)
|
||
|
||
Spacer(modifier = Modifier.height(4.dp))
|
||
OutlinedButton(
|
||
onClick = { /* Link kopieren oder Email Dialog öffnen */ },
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Text("Veranstalter-Link senden")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
} else if (isDesktop && !isAdmin) {
|
||
// DESKTOP VIEW - VERANSTALTER (Meldestelle am Platz)
|
||
Text(
|
||
"Willkommen in der Meldestellen-Software für Turnier 26129",
|
||
style = MaterialTheme.typography.headlineMedium
|
||
)
|
||
Text("Bitte initialisieren Sie die lokale Datenbank oder importieren Sie einen Stand vom USB-Stick.")
|
||
Spacer(Modifier.height(16.dp))
|
||
Button(onClick = onCreateTournament) { // This acts as the "Setup Wizard"
|
||
Text("Turnier initialisieren / Importieren")
|
||
}
|
||
} else if (!isDesktop && isAdmin) {
|
||
// WEB VIEW - ADMIN PORTAL (Deine Steuerungszentrale im Web)
|
||
Text(
|
||
text = "Alle verwalteten Turniere",
|
||
style = MaterialTheme.typography.headlineSmall,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
|
||
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(
|
||
"CDN-C NEU CDNP-C Neumarkt",
|
||
style = MaterialTheme.typography.titleMedium,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
Text("Veranstalter: URFV Neumarkt", style = MaterialTheme.typography.bodyMedium)
|
||
Text("Nr: 26129 | 26. APRIL 2026", style = MaterialTheme.typography.bodyMedium)
|
||
}
|
||
Button(onClick = { /* TODO: Download Trigger for Master App */ }) {
|
||
Text("Master-Desktop-App herunterladen")
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// WEB VIEW - VERANSTALTER PORTAL
|
||
// Top: Aktuelles Turnier & Download
|
||
Card(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
|
||
) {
|
||
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
Text(
|
||
"Aktuelles Turnier: CDN-C NEU CDNP-C Neumarkt",
|
||
style = MaterialTheme.typography.headlineSmall,
|
||
fontWeight = FontWeight.Bold,
|
||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||
)
|
||
Text(
|
||
"Turnier-Nr.: 26129 | 26. APRIL 2026",
|
||
style = MaterialTheme.typography.bodyMedium,
|
||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||
)
|
||
|
||
Spacer(Modifier.height(8.dp))
|
||
|
||
Text(
|
||
"Bitte laden Sie die Desktop-Anwendung herunter, um die Meldestelle lokal an Ihrem Turnierplatz zu betreiben.",
|
||
style = MaterialTheme.typography.bodyLarge,
|
||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||
)
|
||
|
||
Row(
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||
modifier = Modifier.padding(top = 8.dp)
|
||
) {
|
||
Button(onClick = { /* TODO: Download Trigger for generic app */ }) {
|
||
Text("Meldestelle-App herunterladen (.exe)")
|
||
}
|
||
// Lizenz-Key Anzeige
|
||
Surface(
|
||
shape = MaterialTheme.shapes.small,
|
||
color = MaterialTheme.colorScheme.surface,
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
|
||
) {
|
||
Text(
|
||
"Ihr Aktivierungs-Code: X7F9-K2M4",
|
||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||
style = MaterialTheme.typography.labelLarge
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Middle: Historie (Meine Turniere)
|
||
Column(
|
||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Text("Turnier-Historie (Archiv)", style = MaterialTheme.typography.headlineSmall)
|
||
|
||
// Dummy Turniere aus der Vergangenheit
|
||
val turniere = listOf(
|
||
TournamentData(
|
||
id = "25044",
|
||
date = "24. APRIL 2025",
|
||
title = "CSN-C CSNP-C Neumarkt",
|
||
location = "NEUMARKT/M., OÖ"
|
||
),
|
||
TournamentData(
|
||
id = "24012",
|
||
date = "28. APRIL 2024",
|
||
title = "CDN-C CDNP-C Neumarkt",
|
||
location = "NEUMARKT/M., OÖ"
|
||
)
|
||
)
|
||
|
||
turniere.forEach { turnier ->
|
||
Card(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||
) {
|
||
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)
|
||
}
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
OutlinedButton(onClick = { /* TODO: Open PDF Archive */ }) {
|
||
Text("Ergebnislisten (PDF)")
|
||
}
|
||
OutlinedButton(onClick = { /* TODO: Open Stats */ }) {
|
||
Text("Statistiken")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun ToggleRow(label: String, isOnline: Boolean, isInteractive: Boolean = false) {
|
||
Surface(
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||
shape = MaterialTheme.shapes.small,
|
||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||
color = if (isInteractive) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceVariant
|
||
) {
|
||
Row(
|
||
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||
verticalAlignment = Alignment.CenterVertically,
|
||
horizontalArrangement = Arrangement.SpaceBetween
|
||
) {
|
||
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
// Fake status circle
|
||
val statusColor = if (isOnline) Color(0xFF4CAF50) else Color(0xFF9E9E9E)
|
||
Surface(
|
||
modifier = Modifier.size(16.dp),
|
||
shape = androidx.compose.foundation.shape.CircleShape,
|
||
color = statusColor
|
||
) {}
|
||
|
||
// Fake switch / Status text
|
||
if (isInteractive) {
|
||
Surface(
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||
modifier = Modifier.width(40.dp).height(24.dp)
|
||
) {
|
||
Box(contentAlignment = Alignment.Center) {
|
||
Text(if (isOnline) "on" else "off", style = MaterialTheme.typography.labelSmall)
|
||
}
|
||
}
|
||
} else {
|
||
Text(
|
||
if (isOnline) "Online" else "Offline",
|
||
style = MaterialTheme.typography.labelSmall,
|
||
modifier = Modifier.width(40.dp),
|
||
textAlign = TextAlign.Center
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@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 (Desktop Client)",
|
||
style = MaterialTheme.typography.titleLarge,
|
||
fontWeight = FontWeight.Bold
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 = "Transfer", isActive = currentStep == 1, isCompleted = currentStep > 1)
|
||
StepIndicator(step = 2, title = "Stammdaten", isActive = currentStep == 2, isCompleted = currentStep > 2)
|
||
StepIndicator(step = 3, title = "Konfiguration", isActive = currentStep == 3, isCompleted = currentStep > 3)
|
||
StepIndicator(step = 4, title = "Funktionäre", isActive = currentStep == 4, isCompleted = currentStep > 4)
|
||
StepIndicator(step = 5, title = "Bewerbe", isActive = currentStep == 5, isCompleted = currentStep > 5)
|
||
}
|
||
}
|
||
|
||
// Wizard Content Area
|
||
Box(modifier = Modifier.weight(1f).padding(24.dp)) {
|
||
when (currentStep) {
|
||
1 -> TournamentStepTransfer()
|
||
2 -> TournamentStepStammdaten()
|
||
3 -> TournamentStepKonfiguration()
|
||
4 -> TournamentStepFunktionaere()
|
||
5 -> 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 < 5) {
|
||
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 TournamentStepTransfer() {
|
||
Column(verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.fillMaxWidth(0.8f)) {
|
||
Text("Schritt 1: Transfer & Initialisierung", style = MaterialTheme.typography.headlineSmall)
|
||
Text(
|
||
"In diesem Schritt erschaffen wir eine separate Datenbank für dieses spezifische Turnier. " +
|
||
"Diese Datenbank kann für den komplett isolierten Offline-Betrieb (z.B. am USB-Stick) auf andere Laptops übertragen werden.",
|
||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||
)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Turniernummer OEPS (z.B. 26128)") },
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
Button(onClick = { /*TODO*/ }, modifier = Modifier.align(Alignment.CenterVertically)) {
|
||
Text("Turnierdatenbank Initialisieren")
|
||
}
|
||
}
|
||
|
||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||
|
||
Text("Datenaustausch (OEPS / Externe Systeme)", style = MaterialTheme.typography.titleMedium)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Card(modifier = Modifier.weight(1f)) {
|
||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||
Text("ZNS / Stammdaten Import", fontWeight = FontWeight.Bold)
|
||
Text(
|
||
"Aktualisieren Sie die Reiter, Pferde und Funktionäre aus dem zentralen System.",
|
||
style = MaterialTheme.typography.bodyMedium
|
||
)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Pfad zur ZNS.zip") },
|
||
modifier = Modifier.weight(1f),
|
||
readOnly = true
|
||
)
|
||
Button(onClick = { /* Öffnet File Picker */ }) { Text("...") }
|
||
}
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Pfad zur AWÖ/Zucht-Datei") },
|
||
modifier = Modifier.weight(1f),
|
||
readOnly = true
|
||
)
|
||
Button(onClick = { /* Öffnet File Picker */ }) { Text("...") }
|
||
}
|
||
}
|
||
}
|
||
|
||
Card(modifier = Modifier.weight(1f)) {
|
||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||
Text("OEPS Export", fontWeight = FontWeight.Bold)
|
||
Text(
|
||
"Erzeugt die xxxxx.erg Datei für die offizielle Ergebnismeldung nach dem Turnier.",
|
||
style = MaterialTheme.typography.bodyMedium
|
||
)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Ziel-Ordner für .erg Export") },
|
||
modifier = Modifier.weight(1f),
|
||
readOnly = true
|
||
)
|
||
Button(onClick = { /* Öffnet Directory Picker */ }) { Text("...") }
|
||
}
|
||
Button(
|
||
onClick = { /*TODO*/ },
|
||
modifier = Modifier.fillMaxWidth(),
|
||
enabled = false
|
||
) { Text("Ergebnis-Export (.erg)") }
|
||
}
|
||
}
|
||
}
|
||
|
||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||
|
||
Text("Offline-Sync (USB-Stick / Lokales Netzwerk)", style = MaterialTheme.typography.titleMedium)
|
||
Text(
|
||
"Übertragen Sie den kompletten Turnierstand zwischen Master-Meldestelle und Richterturm-Laptops ohne Internet.",
|
||
style = MaterialTheme.typography.bodyMedium,
|
||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||
)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
// Export (Speichern auf Stick)
|
||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Ziel-Pfad (z.B. D:/Export)") },
|
||
modifier = Modifier.weight(1f),
|
||
readOnly = true
|
||
)
|
||
Button(onClick = { /* Öffnet Directory Picker */ }) { Text("...") }
|
||
}
|
||
OutlinedButton(onClick = { /*TODO*/ }, modifier = Modifier.fillMaxWidth()) { Text("↑ Turnier Exportieren") }
|
||
}
|
||
|
||
// Import (Lesen vom Stick)
|
||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||
OutlinedTextField(
|
||
value = "",
|
||
onValueChange = {},
|
||
label = { Text("Quell-Datei (z.B. D:/turnier.db)") },
|
||
modifier = Modifier.weight(1f),
|
||
readOnly = true
|
||
)
|
||
Button(onClick = { /* Öffnet File Picker */ }) { Text("...") }
|
||
}
|
||
OutlinedButton(onClick = { /*TODO*/ }, modifier = Modifier.fillMaxWidth()) { Text("↓ Turnier Importieren") }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun TournamentStepStammdaten() {
|
||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
|
||
Text("Schritt 2: Turnier-Stammdaten", style = MaterialTheme.typography.headlineSmall)
|
||
|
||
OutlinedTextField(
|
||
value = "CSN-C NEU Neumarkt", // Dummy pre-fill
|
||
onValueChange = {},
|
||
label = { Text("Turniername") },
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth()) {
|
||
OutlinedTextField(
|
||
value = "25.04.2026",
|
||
onValueChange = {},
|
||
label = { Text("Datum von") },
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
OutlinedTextField(
|
||
value = "25.04.2026",
|
||
onValueChange = {},
|
||
label = { Text("Datum bis") },
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun TournamentStepKonfiguration() {
|
||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
|
||
Text("Schritt 3: 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/Vlies 45x65m)") })
|
||
FilterChip(selected = false, onClick = {}, label = { Text("Halle (Sand/Vlies 20x40m)") })
|
||
FilterChip(selected = false, onClick = {}, label = { Text("+ Hinzufügen") })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun TournamentStepFunktionaere() {
|
||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(0.6f)) {
|
||
Text("Schritt 4: Team & Funktionäre", style = MaterialTheme.typography.headlineSmall)
|
||
Text("Zuweisung von Richtern und Parcoursbauern (aus ZNS)")
|
||
|
||
OutlinedTextField(
|
||
value = "Rudi Kreupl",
|
||
onValueChange = {},
|
||
label = { Text("Turnierbeauftragter (Suche nach Name oder ID)") },
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
OutlinedTextField(
|
||
value = "Helmut Riedler",
|
||
onValueChange = {},
|
||
label = { Text("Richter (Suche nach Name oder ID)") },
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
OutlinedTextField(
|
||
value = "Kurt Reitetschlägerr",
|
||
onValueChange = {},
|
||
label = { Text("Parcoursbauchef") },
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun TournamentStepBewerbe() {
|
||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxSize()) {
|
||
Text("Schritt 5: 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("3: Pony Stilspringprüfung (70cm)", modifier = Modifier.padding(vertical = 4.dp))
|
||
Text("4: Einlaufspringprüfung (70cm)", 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")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@OptIn(ExperimentalMaterial3Api::class)
|
||
@Composable
|
||
private fun OrganizerProfileScreen(
|
||
authTokenManager: AuthTokenManager,
|
||
onLogout: () -> Unit,
|
||
onNavigateToDashboard: () -> Unit
|
||
) {
|
||
val authState by authTokenManager.authState.collectAsState()
|
||
val scrollState = rememberScrollState()
|
||
|
||
// Formular-Felder
|
||
var vereinsname by remember { mutableStateOf("URFV Neumarkt") }
|
||
var vereinskuerzel by remember { mutableStateOf("URFV") }
|
||
var adresse by remember { mutableStateOf("") }
|
||
var plz by remember { mutableStateOf("") }
|
||
var ort by remember { mutableStateOf("") }
|
||
var land by remember { mutableStateOf("Österreich") }
|
||
var mapsLink by remember { mutableStateOf("") }
|
||
|
||
// Ansprechpersonen
|
||
var kontakt1Name by remember { mutableStateOf("") }
|
||
var kontakt1Email by remember { mutableStateOf("") }
|
||
var kontakt1Telefon by remember { mutableStateOf("") }
|
||
var kontakt2Name by remember { mutableStateOf("") }
|
||
var kontakt2Email by remember { mutableStateOf("") }
|
||
var kontakt2Telefon by remember { mutableStateOf("") }
|
||
|
||
// Social / Links
|
||
var webseite by remember { mutableStateOf("") }
|
||
var facebook by remember { mutableStateOf("") }
|
||
var instagram by remember { mutableStateOf("") }
|
||
var youtube by remember { mutableStateOf("") }
|
||
|
||
// Weitere Infos
|
||
var vereinsbeschreibung by remember { mutableStateOf("") }
|
||
var bankverbindung by remember { mutableStateOf("") }
|
||
var uid by remember { mutableStateOf("") }
|
||
|
||
var saveSuccess by remember { mutableStateOf(false) }
|
||
|
||
Scaffold(
|
||
topBar = {
|
||
TopAppBar(
|
||
title = { Text("Veranstalter Profil") },
|
||
navigationIcon = {
|
||
IconButton(onClick = onNavigateToDashboard) {
|
||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||
}
|
||
},
|
||
actions = {
|
||
Text(
|
||
text = authState.username ?: "",
|
||
style = MaterialTheme.typography.bodyMedium,
|
||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||
modifier = Modifier.padding(end = 8.dp)
|
||
)
|
||
TextButton(onClick = onLogout) { Text("Abmelden") }
|
||
}
|
||
)
|
||
}
|
||
) { paddingValues ->
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(paddingValues)
|
||
.verticalScroll(scrollState)
|
||
.padding(24.dp),
|
||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||
) {
|
||
|
||
// --- Logo & Vereinsname ---
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Text("Verein / Veranstalter", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||
|
||
// Logo Placeholder
|
||
Surface(
|
||
modifier = Modifier.fillMaxWidth(),
|
||
shape = MaterialTheme.shapes.medium,
|
||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||
) {
|
||
Column(
|
||
modifier = Modifier.padding(24.dp).fillMaxWidth(),
|
||
horizontalAlignment = Alignment.CenterHorizontally,
|
||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||
) {
|
||
Text("🏆", style = MaterialTheme.typography.displayMedium)
|
||
Text("Vereins-/Veranstaltungslogo", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||
OutlinedButton(onClick = { /* TODO: File Picker */ }) {
|
||
Text("Logo hochladen")
|
||
}
|
||
}
|
||
}
|
||
|
||
OutlinedTextField(
|
||
value = vereinsname,
|
||
onValueChange = { vereinsname = it },
|
||
label = { Text("Vereinsname / Veranstalter") },
|
||
singleLine = true,
|
||
modifier = androidx.compose.ui.Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = vereinskuerzel,
|
||
onValueChange = { vereinskuerzel = it },
|
||
label = { Text("Kürzel") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = vereinsbeschreibung,
|
||
onValueChange = { vereinsbeschreibung = it },
|
||
label = { Text("Kurzbeschreibung / Über uns") },
|
||
minLines = 3,
|
||
maxLines = 6,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
// --- Adresse ---
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Text("Adresse", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||
|
||
OutlinedTextField(
|
||
value = adresse,
|
||
onValueChange = { adresse = it },
|
||
label = { Text("Straße & Hausnummer") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||
OutlinedTextField(
|
||
value = plz,
|
||
onValueChange = { plz = it },
|
||
label = { Text("PLZ") },
|
||
singleLine = true,
|
||
modifier = Modifier.width(100.dp)
|
||
)
|
||
OutlinedTextField(
|
||
value = ort,
|
||
onValueChange = { ort = it },
|
||
label = { Text("Ort") },
|
||
singleLine = true,
|
||
modifier = Modifier.weight(1f)
|
||
)
|
||
}
|
||
OutlinedTextField(
|
||
value = land,
|
||
onValueChange = { land = it },
|
||
label = { Text("Land") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = mapsLink,
|
||
onValueChange = { mapsLink = it },
|
||
label = { Text("Google Maps / OpenStreetMap Link") },
|
||
placeholder = { Text("https://maps.google.com/...") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
// --- Ansprechpersonen ---
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Text("Ansprechpersonen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||
|
||
Text("Hauptkontakt", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||
OutlinedTextField(
|
||
value = kontakt1Name,
|
||
onValueChange = { kontakt1Name = it },
|
||
label = { Text("Name") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = kontakt1Email,
|
||
onValueChange = { kontakt1Email = it },
|
||
label = { Text("E-Mail") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = kontakt1Telefon,
|
||
onValueChange = { kontakt1Telefon = it },
|
||
label = { Text("Telefon / Mobil") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
|
||
HorizontalDivider()
|
||
|
||
Text("Weiterer Kontakt (optional)", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.secondary)
|
||
OutlinedTextField(
|
||
value = kontakt2Name,
|
||
onValueChange = { kontakt2Name = it },
|
||
label = { Text("Name") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = kontakt2Email,
|
||
onValueChange = { kontakt2Email = it },
|
||
label = { Text("E-Mail") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = kontakt2Telefon,
|
||
onValueChange = { kontakt2Telefon = it },
|
||
label = { Text("Telefon / Mobil") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
// --- Social Media & Links ---
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Text("Links & Social Media", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||
|
||
OutlinedTextField(
|
||
value = webseite,
|
||
onValueChange = { webseite = it },
|
||
label = { Text("Webseite") },
|
||
placeholder = { Text("https://www.meinverein.at") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = facebook,
|
||
onValueChange = { facebook = it },
|
||
label = { Text("Facebook") },
|
||
placeholder = { Text("https://facebook.com/...") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = instagram,
|
||
onValueChange = { instagram = it },
|
||
label = { Text("Instagram") },
|
||
placeholder = { Text("https://instagram.com/...") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = youtube,
|
||
onValueChange = { youtube = it },
|
||
label = { Text("YouTube") },
|
||
placeholder = { Text("https://youtube.com/...") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
// --- Weitere Vereinsdaten ---
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Text("Weitere Informationen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||
|
||
OutlinedTextField(
|
||
value = bankverbindung,
|
||
onValueChange = { bankverbindung = it },
|
||
label = { Text("IBAN / Bankverbindung") },
|
||
placeholder = { Text("AT12 3456 7890 1234 5678") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
OutlinedTextField(
|
||
value = uid,
|
||
onValueChange = { uid = it },
|
||
label = { Text("UID-Nummer / ZVR-Zahl") },
|
||
singleLine = true,
|
||
modifier = Modifier.fillMaxWidth()
|
||
)
|
||
}
|
||
}
|
||
|
||
// --- Speichern ---
|
||
if (saveSuccess) {
|
||
Card(
|
||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Text(
|
||
"✓ Profil erfolgreich gespeichert!",
|
||
modifier = Modifier.padding(16.dp),
|
||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||
style = MaterialTheme.typography.bodyLarge
|
||
)
|
||
}
|
||
}
|
||
|
||
Button(
|
||
onClick = {
|
||
// TODO: Backend-Anbindung (PUT /api/organizer/profile)
|
||
saveSuccess = true
|
||
},
|
||
modifier = Modifier.fillMaxWidth().height(52.dp)
|
||
) {
|
||
Text("Profil speichern", style = MaterialTheme.typography.titleMedium)
|
||
}
|
||
|
||
OutlinedButton(
|
||
onClick = onNavigateToDashboard,
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Text("Zum Dashboard")
|
||
}
|
||
|
||
Spacer(modifier = Modifier.height(24.dp))
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun AuthStatusScreen(
|
||
authTokenManager: AuthTokenManager,
|
||
onNavigateToLogin: () -> Unit,
|
||
onNavigateToPing: () -> Unit,
|
||
onBackToHome: () -> Unit
|
||
) {
|
||
val authState by authTokenManager.authState.collectAsState()
|
||
Column(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(24.dp),
|
||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||
) {
|
||
Text("Ping-Service / System Status", style = MaterialTheme.typography.headlineMedium)
|
||
|
||
Card(modifier = Modifier.fillMaxWidth()) {
|
||
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
if (authState.isAuthenticated) {
|
||
Text(
|
||
"Du bist angemeldet als: ${authState.username ?: authState.userId ?: "unbekannt"}",
|
||
style = MaterialTheme.typography.titleMedium,
|
||
color = MaterialTheme.colorScheme.primary
|
||
)
|
||
Spacer(Modifier.height(8.dp))
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Button(onClick = onNavigateToPing) {
|
||
Text("Ping-Service Tests durchführen")
|
||
}
|
||
OutlinedButton(onClick = {
|
||
authTokenManager.clearToken()
|
||
}) { Text("Abmelden") }
|
||
}
|
||
|
||
} else {
|
||
Text(
|
||
"Du bist abgemeldet.",
|
||
style = MaterialTheme.typography.titleMedium,
|
||
color = MaterialTheme.colorScheme.error
|
||
)
|
||
Spacer(Modifier.height(8.dp))
|
||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||
Button(onClick = onNavigateToLogin) {
|
||
Text("Login")
|
||
}
|
||
OutlinedButton(onClick = onNavigateToPing) {
|
||
Text("Ping-Service (eingeschränkt testen)")
|
||
}
|
||
}
|
||
}
|
||
|
||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||
OutlinedButton(onClick = onBackToHome) { Text("Zurück zur Startseite") }
|
||
}
|
||
}
|
||
}
|
||
}
|