feat(ui): add new screens for AuthStatus, OrganizerProfile, and Dashboard

- Introduced `AuthStatusScreen` to display user authentication status and navigation options.
- Added `OrganizerProfileScreen` with detailed profile editing features (contacts, address, and social links).
- Created `DashboardScreen` for admins and organizers with responsive layouts.
- Developed reusable `TournamentCard` component for displaying tournament details.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-21 18:37:23 +01:00
parent 9882109d16
commit e89c58bd28
7 changed files with 1365 additions and 1317 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
package components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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
// Dummy-Datenklasse für Turnier-Einträge (wird später durch echtes Domain-Model ersetzt)
data class TournamentData(
val id: String,
val date: String,
val title: String,
val location: String
)
@Composable
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
fun ToggleRow(label: String, isOnline: Boolean, isInteractive: Boolean = false) {
Surface(
border = 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)) {
val statusColor = if (isOnline) Color(0xFF4CAF50) else Color(0xFF9E9E9E)
Surface(
modifier = Modifier.size(16.dp),
shape = CircleShape,
color = statusColor
) {}
if (isInteractive) {
Surface(
border = 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
)
}
}
}
}
}
@@ -0,0 +1,68 @@
package screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.auth.data.AuthTokenManager
@Composable
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") }
}
}
}
}
@@ -0,0 +1,390 @@
package screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun CreateTournamentScreen(
onBack: () -> Unit,
onSave: () -> Unit
) {
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))
}
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)) {
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") }
}
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",
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")
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 {
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") })
}
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")
}
}
}
}
}
}
}
@@ -0,0 +1,382 @@
package screens
import androidx.compose.foundation.BorderStroke
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 at.mocode.frontend.core.auth.data.AuthTokenManager
import at.mocode.frontend.core.domain.PlatformType
import at.mocode.frontend.core.domain.currentPlatform
import components.ToggleRow
import components.TournamentData
@Composable
fun DashboardScreen(
authTokenManager: AuthTokenManager,
onLogout: () -> Unit,
onCreateTournament: () -> Unit,
onNennungOeffnen: () -> Unit = {},
) {
val authState by authTokenManager.authState.collectAsState()
val scrollState = rememberScrollState()
val isDesktop = currentPlatform() == PlatformType.DESKTOP
// Security Check für das Dashboard
if (!authState.isAuthenticated) {
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
Button(
onClick = onNennungOeffnen,
modifier = Modifier.fillMaxWidth().height(64.dp)
) {
Text(
text = "📋 Nennungs-Maske öffnen",
style = MaterialTheme.typography.titleMedium
)
}
OutlinedButton(
onClick = onCreateTournament,
modifier = Modifier.fillMaxWidth().height(64.dp)
) {
Text(
text = "+ neues Turnier anlegen",
style = MaterialTheme.typography.titleMedium
)
}
Text(
text = "Alle verwalteten Turniere",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
// Filters (Mockup)
Surface(
border = 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))
) {
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 = 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
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.weight(1f)
) {
Surface(
modifier = Modifier.size(120.dp),
border = 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)
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
var meldestelleOnline by remember { mutableStateOf(true) }
var nennsystemOnline by remember { mutableStateOf(true) }
var startlisteOnline by remember { mutableStateOf(true) }
ToggleRow("Meldestelle-Desktop online", isOnline = meldestelleOnline, isInteractive = false)
ToggleRow("Nennsystem online", isOnline = nennsystemOnline, isInteractive = true)
ToggleRow("Start- Ergebnislisten online", isOnline = startlisteOnline, 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) {
Text("Turnier initialisieren / Importieren")
}
} else if (!isDesktop && isAdmin) {
// WEB VIEW - ADMIN PORTAL
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
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)")
}
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surface,
border = 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
)
}
}
}
}
// Turnier-Historie
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
Text("Turnier-Historie (Archiv)", style = MaterialTheme.typography.headlineSmall)
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 = 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")
}
}
}
}
}
}
}
}
}
}
@@ -1,3 +1,5 @@
package screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -0,0 +1,345 @@
package screens
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.auth.data.AuthTokenManager
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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 = 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 = 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))
}
}
}