refactor(ui, navigation): implement platform-specific routing and redesign components
- Added platform detection logic `currentPlatform()` in `PlatformType.js.kt`. - Introduced platform-based behavior for LandingScreen, Dashboard, and Login flow. - Replaced Row with FlowRow in PingScreen to improve button layout. - Updated Meldestelle Dashboard with platform-specific headers and authentication checks. - Adjusted AppHeader to accept `isAuthenticated` and `username` parameters. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -5,13 +5,17 @@ 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.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
|
||||
@@ -38,27 +42,58 @@ fun MainApp() {
|
||||
val pingViewModel: PingViewModel = koinViewModel()
|
||||
val loginViewModel: LoginViewModel = koinViewModel()
|
||||
|
||||
when (currentScreen) {
|
||||
is AppScreen.Landing -> LandingScreen(
|
||||
onPrimaryCta = { navigationPort.navigateToScreen(AppScreen.Login) }
|
||||
)
|
||||
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.Profile) } // Open the Ping Overview / Status page
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AppScreen.Dashboard -> DashboardScreen(
|
||||
authTokenManager = authTokenManager,
|
||||
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
|
||||
onLogout = {
|
||||
authTokenManager.clearToken()
|
||||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||||
}
|
||||
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,
|
||||
onOpenPing = { navigationPort.navigateToScreen(AppScreen.Ping) },
|
||||
onLogout = {
|
||||
authTokenManager.clearToken()
|
||||
navigationPort.navigateToScreen(AppScreen.Landing)
|
||||
}
|
||||
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(
|
||||
@@ -66,20 +101,47 @@ fun MainApp() {
|
||||
onSave = { navigationPort.navigateToScreen(AppScreen.Dashboard) } // Later we go to tournament detail
|
||||
)
|
||||
|
||||
is AppScreen.Login -> LoginScreen(
|
||||
viewModel = loginViewModel,
|
||||
onLoginSuccess = { navigationPort.navigateToScreen(AppScreen.Dashboard) },
|
||||
onBack = { navigationPort.navigateToScreen(AppScreen.Landing) }
|
||||
)
|
||||
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.Dashboard) }
|
||||
onBack = { navigationPort.navigateToScreen(AppScreen.Profile) } // Always go back to overview
|
||||
)
|
||||
|
||||
is AppScreen.Profile -> AuthStatusScreen(
|
||||
authTokenManager = authTokenManager,
|
||||
onBackToHome = { navigationPort.navigateToScreen(AppScreen.Dashboard) }
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
else -> {}
|
||||
@@ -90,7 +152,8 @@ fun MainApp() {
|
||||
|
||||
@Composable
|
||||
private fun LandingScreen(
|
||||
onPrimaryCta: () -> Unit
|
||||
onPrimaryCta: () -> Unit,
|
||||
onOpenPing: () -> Unit
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -142,41 +205,6 @@ private fun LandingScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// --- AKTUELLE TURNIERE SECTION ---
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 40.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Text("Aktuelle Turniere", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
// Dummy Daten basierend auf Neumarkt 2026
|
||||
val turniere = listOf(
|
||||
TournamentData(
|
||||
id = "26128",
|
||||
date = "25. APRIL 2026",
|
||||
title = "CSN-C NEU CSNP-C NEU",
|
||||
location = "NEUMARKT/M., OÖ"
|
||||
),
|
||||
TournamentData(
|
||||
id = "26129",
|
||||
date = "26. APRIL 2026",
|
||||
title = "CDN-C NEU CDNP-C NEU",
|
||||
location = "NEUMARKT/M., OÖ"
|
||||
)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
turniere.forEach { turnier ->
|
||||
TournamentCard(turnier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest / Intro
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
Column(
|
||||
@@ -233,6 +261,18 @@ private fun LandingScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
@@ -355,10 +395,24 @@ private fun FeatureCard(number: String, title: String, body: String) {
|
||||
@Composable
|
||||
private fun DashboardScreen(
|
||||
authTokenManager: AuthTokenManager,
|
||||
onOpenPing: () -> Unit,
|
||||
onLogout: () -> Unit
|
||||
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()
|
||||
@@ -375,7 +429,7 @@ private fun DashboardScreen(
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Meldestelle Dashboard",
|
||||
text = if (isDesktop) "Master-Meldestelle Steuerungszentrale" else if (isAdmin) "Admin-Dashboard (Web)" else "Veranstalter-Dashboard",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -396,112 +450,351 @@ private fun DashboardScreen(
|
||||
}
|
||||
|
||||
// Main Content Area
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp).verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// Left Column (Turniere)
|
||||
Column(
|
||||
modifier = Modifier.weight(2f),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Meine Turniere", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
// Dummy Turniere für die Meldestelle
|
||||
val turniere = listOf(
|
||||
TournamentData(
|
||||
id = "26128",
|
||||
date = "25. APRIL 2026",
|
||||
title = "CSN-C NEU CSNP-C NEU",
|
||||
location = "NEUMARKT/M., OÖ"
|
||||
),
|
||||
TournamentData(
|
||||
id = "26129",
|
||||
date = "26. APRIL 2026",
|
||||
title = "CDN-C NEU CDNP-C NEU",
|
||||
location = "NEUMARKT/M., OÖ"
|
||||
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
|
||||
)
|
||||
|
||||
turniere.forEach { turnier ->
|
||||
// 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(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
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(turnier.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Text("Nr: ${turnier.id} | ${turnier.date}", style = MaterialTheme.typography.bodyMedium)
|
||||
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: Open Meldestellen Cockpit for this tournament */ }) {
|
||||
Text("Meldestelle öffnen")
|
||||
Button(onClick = { /* TODO: Download Trigger for Master App */ }) {
|
||||
Text("Master-Desktop-App herunterladen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEIN NEUES KONZEPT: Download Desktop App statt "Neues Turnier anlegen" im Web
|
||||
} else {
|
||||
// WEB VIEW - VERANSTALTER PORTAL
|
||||
// Top: Aktuelles Turnier & Download
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
"Master-Meldestelle Desktop App",
|
||||
"Aktuelles Turnier: CDN-C NEU CDNP-C Neumarkt",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Text(
|
||||
"Für die Turnieranlage, den ZNS-Import und vollen Offline-Betrieb (USB-Sync) laden Sie bitte die Desktop-Anwendung herunter.",
|
||||
"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 */ }) {
|
||||
Text("Download für Windows (.exe)")
|
||||
Button(onClick = { /* TODO: Download Trigger for generic app */ }) {
|
||||
Text("Meldestelle-App herunterladen (.exe)")
|
||||
}
|
||||
// Status Anzeige
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
// Lizenz-Key Anzeige
|
||||
Surface(
|
||||
modifier = Modifier.size(12.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
) {}
|
||||
Text("Desktop App derzeit Offline", style = MaterialTheme.typography.labelMedium)
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right Column (System / Tools)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("System & Tools", style = MaterialTheme.typography.headlineSmall)
|
||||
@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
|
||||
) {}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = onOpenPing,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Ping-Service (System Status)")
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,6 +1191,8 @@ fun TournamentStepBewerbe() {
|
||||
@Composable
|
||||
private fun AuthStatusScreen(
|
||||
authTokenManager: AuthTokenManager,
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onNavigateToPing: () -> Unit,
|
||||
onBackToHome: () -> Unit
|
||||
) {
|
||||
val authState by authTokenManager.authState.collectAsState()
|
||||
@@ -907,24 +1202,45 @@ private fun AuthStatusScreen(
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Profil / Status", style = MaterialTheme.typography.headlineMedium)
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
if (authState.isAuthenticated) {
|
||||
Text("Du bist als ${authState.username ?: authState.userId ?: "unbekannt"} angemeldet.")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = {
|
||||
authTokenManager.clearToken()
|
||||
onBackToHome()
|
||||
}) { Text("Abmelden") }
|
||||
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))
|
||||
OutlinedButton(onClick = onBackToHome) { Text("Zurück zum Dashboard") }
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Button(onClick = onNavigateToPing) {
|
||||
Text("Ping-Service Tests durchführen")
|
||||
}
|
||||
OutlinedButton(onClick = {
|
||||
authTokenManager.clearToken()
|
||||
}) { Text("Abmelden") }
|
||||
}
|
||||
|
||||
} else {
|
||||
Text("Nicht angemeldet.")
|
||||
Text(
|
||||
"Du bist abgemeldet.",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = onBackToHome) { Text("Zurück") }
|
||||
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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ fun main() = application {
|
||||
}
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Equest-Events Master Desktop",
|
||||
title = "Master Desktop",
|
||||
state = WindowState(width = 1200.dp, height = 800.dp)
|
||||
) {
|
||||
MainApp()
|
||||
|
||||
Reference in New Issue
Block a user