meldestelle/frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt
StefanMoCoAt 575ef18034
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
Refactor: Replace androidx.compose.ui.Modifier with Modifier for cleaner code and consistency
2026-03-21 00:01:53 +01:00

1585 lines
59 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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") }
}
}
}
}