Implement Veranstalter and Veranstaltung management: Add VeranstalterDetailScreen, seed FakeVeranstaltungStore, and enable deletion of Veranstaltungen. Extend onboarding with device name validation. Refine UI for VeranstalterKonfigScreen, add InvalidContextNotice, and centralize navigation checks.
This commit is contained in:
+4
-3
@@ -36,10 +36,11 @@ fun DesktopApp() {
|
||||
|
||||
val authState by authTokenManager.authState.collectAsState()
|
||||
|
||||
// Login-Gate: Nicht-authentifizierte Screens → Login
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login) {
|
||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding) {
|
||||
LaunchedEffect(Unit) {
|
||||
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.Veranstaltungen))
|
||||
// Wenn noch keine Authentifizierung vorhanden ist, zuerst Onboarding anzeigen
|
||||
nav.navigateToScreen(AppScreen.Onboarding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ fun main() = application {
|
||||
Window(
|
||||
onCloseRequest = ::exitApplication,
|
||||
title = "Meldestelle",
|
||||
state = WindowState(width = 1400.dp, height = 900.dp),
|
||||
state = WindowState(width = 1600.dp, height = 900.dp),
|
||||
) {
|
||||
DesktopApp()
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
* Hält den aktuellen Screen als StateFlow, den DesktopApp beobachtet.
|
||||
*/
|
||||
class DesktopNavigationPort : NavigationPort {
|
||||
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.Login())
|
||||
private val _currentScreen = MutableStateFlow<AppScreen>(AppScreen.Onboarding)
|
||||
override val currentScreen: StateFlow<AppScreen> = _currentScreen.asStateFlow()
|
||||
|
||||
override fun navigateTo(route: String) {
|
||||
|
||||
+138
-30
@@ -3,6 +3,8 @@ package at.mocode.desktop.screens.layout
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
@@ -24,6 +26,8 @@ import at.mocode.turnier.feature.presentation.TurnierNeuScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore
|
||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||
@@ -264,6 +268,19 @@ private fun BreadcrumbSeparator() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InvalidContextNotice(message: String, onBack: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(message, color = Color(0xFFB91C1C), fontSize = 14.sp)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(onClick = onBack) { Text("Zur Auswahl") }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-Bereich: rendert den passenden Screen je nach aktuellem AppScreen.
|
||||
*/
|
||||
@@ -273,6 +290,18 @@ private fun DesktopContentArea(
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
) {
|
||||
when (currentScreen) {
|
||||
// Onboarding ohne Login
|
||||
is AppScreen.Onboarding -> {
|
||||
val authTokenManager: at.mocode.frontend.core.auth.data.AuthTokenManager = koinInject()
|
||||
at.mocode.desktop.screens.onboarding.OnboardingScreen(
|
||||
onContinue = { _, _, _ ->
|
||||
// Dummy-Token setzen, damit nach Onboarding der Admin-Flow sichtbar ist
|
||||
authTokenManager.setToken("dummy.jwt.token")
|
||||
onNavigate(AppScreen.Veranstaltungen)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Root-Screen: Admin-Übersicht
|
||||
is AppScreen.Veranstaltungen -> AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
@@ -291,26 +320,85 @@ private fun DesktopContentArea(
|
||||
onAbbrechen = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onSpeichern = { _, _, _ -> onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
)
|
||||
is AppScreen.VeranstalterDetail -> VeranstalterDetailScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { vId ->
|
||||
onNavigate(AppScreen.VeranstaltungUebersicht(currentScreen.veranstalterId, vId))
|
||||
},
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||
)
|
||||
is AppScreen.VeranstaltungUebersicht -> VeranstaltungUebersichtScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId)) },
|
||||
onTurnierOeffnen = { tId ->
|
||||
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, tId))
|
||||
},
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.veranstaltungId)) },
|
||||
onZnsImport = { /* TODO: ZNS-Import Dialog für Turnier */ },
|
||||
onDbImport = { /* TODO: DB-Import Dialog */ },
|
||||
onDbExport = { /* TODO: DB-Export Dialog */ },
|
||||
)
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
if (!FakeVeranstalterStore.exists(vId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||
)
|
||||
} else {
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = vId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { evtId ->
|
||||
onNavigate(AppScreen.VeranstaltungUebersicht(vId, evtId))
|
||||
},
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is AppScreen.VeranstaltungKonfig -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
if (!FakeVeranstalterStore.exists(vId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||
)
|
||||
} else {
|
||||
at.mocode.veranstalter.feature.presentation.VeranstaltungKonfigScreen(
|
||||
veranstalterId = vId,
|
||||
onAbbrechen = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
||||
onSpeichern = { titel, datumVon, datumBis ->
|
||||
// ID generieren und in den Fake-Store einfügen
|
||||
val id = System.currentTimeMillis()
|
||||
at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore.addFirst(
|
||||
vId,
|
||||
at.mocode.veranstalter.feature.presentation.VeranstaltungListUiModel(
|
||||
id = id,
|
||||
name = titel,
|
||||
datum = if (datumBis.isNotBlank()) "$datumVon – $datumBis" else datumVon,
|
||||
ort = "",
|
||||
turnierAnzahl = 1,
|
||||
nennungen = 0,
|
||||
bewerbe = 0,
|
||||
letzteAktivitaet = "soeben",
|
||||
status = at.mocode.frontend.core.designsystem.models.VeranstaltungStatus.VORBEREITUNG,
|
||||
)
|
||||
)
|
||||
onNavigate(AppScreen.VeranstalterDetail(vId))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AppScreen.VeranstaltungUebersicht -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
if (!FakeVeranstalterStore.exists(vId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||
)
|
||||
} else if (!FakeVeranstaltungStore.belongsTo(vId, evtId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
|
||||
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
|
||||
)
|
||||
} else {
|
||||
VeranstaltungUebersichtScreen(
|
||||
veranstalterId = vId,
|
||||
veranstaltungId = evtId,
|
||||
onZurueck = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
||||
onTurnierOeffnen = { tId ->
|
||||
onNavigate(AppScreen.TurnierDetail(evtId, tId))
|
||||
},
|
||||
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) },
|
||||
onZnsImport = { /* TODO */ },
|
||||
onDbImport = { /* TODO */ },
|
||||
onDbExport = { /* TODO */ },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Veranstaltungs-Screens
|
||||
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
|
||||
@@ -325,16 +413,36 @@ private fun DesktopContentArea(
|
||||
)
|
||||
|
||||
// Turnier-Screens
|
||||
is AppScreen.TurnierDetail -> TurnierDetailScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
is AppScreen.TurnierNeu -> TurnierNeuScreen(
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId)) },
|
||||
)
|
||||
is AppScreen.TurnierDetail -> {
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
if (!FakeVeranstaltungStore.exists(evtId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||
)
|
||||
} else {
|
||||
TurnierDetailScreen(
|
||||
veranstaltungId = evtId,
|
||||
turnierId = currentScreen.turnierId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
is AppScreen.TurnierNeu -> {
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
if (!FakeVeranstaltungStore.exists(evtId)) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
||||
onBack = { onNavigate(AppScreen.Veranstaltungen) }
|
||||
)
|
||||
} else {
|
||||
TurnierNeuScreen(
|
||||
veranstaltungId = evtId,
|
||||
onBack = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
||||
onSave = { onNavigate(AppScreen.VeranstaltungDetail(evtId)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Ping-Screen
|
||||
is AppScreen.Ping -> {
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package at.mocode.desktop.screens.onboarding
|
||||
|
||||
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.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
enum class ZnsStatus { NONE, LOCAL, SYNCED }
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
initialName: String = "",
|
||||
initialKey: String = "",
|
||||
initialZns: ZnsStatus = ZnsStatus.NONE,
|
||||
onZnsSync: () -> Unit = {},
|
||||
onZnsUsb: () -> Unit = {},
|
||||
onContinue: (geraetName: String, sharedKey: String, znsStatus: ZnsStatus) -> Unit,
|
||||
) {
|
||||
var geraetName by remember { mutableStateOf(initialName) }
|
||||
var sharedKey by remember { mutableStateOf(initialKey) }
|
||||
var znsStatus by remember { mutableStateOf(initialZns) }
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val nameValid = geraetName.trim().length >= 3
|
||||
val keyValid = sharedKey.trim().length >= 8
|
||||
val canContinue = nameValid && keyValid
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Onboarding", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
Card {
|
||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Gerätename (Pflicht)", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = geraetName,
|
||||
onValueChange = { geraetName = it },
|
||||
placeholder = { Text("z. B. Meldestelle") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
isError = !nameValid && geraetName.isNotBlank()
|
||||
)
|
||||
|
||||
Text("Sicherheitsschlüssel (Pflicht)", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = sharedKey,
|
||||
onValueChange = { sharedKey = it },
|
||||
placeholder = { Text("z. B. Neumarkt2026") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
isError = !keyValid && sharedKey.isNotBlank(),
|
||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
val label = if (showPassword) "Verbergen" else "Anzeigen"
|
||||
TextButton(onClick = { showPassword = !showPassword }) {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Text("ZNS-Daten (optional)", style = MaterialTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
AssistChip(onClick = {
|
||||
znsStatus = ZnsStatus.SYNCED
|
||||
onZnsSync()
|
||||
}, label = { Text("Aktualisieren") })
|
||||
AssistChip(onClick = {
|
||||
znsStatus = ZnsStatus.LOCAL
|
||||
onZnsUsb()
|
||||
}, label = { Text("USB-Import") })
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Status: $znsStatus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(onClick = { onContinue(geraetName.trim(), sharedKey.trim(), znsStatus) }, enabled = canContinue) {
|
||||
Text("Weiter zu den Veranstaltungen")
|
||||
}
|
||||
if (!canContinue) {
|
||||
Text("Bitte Gerätename (min. 3) und Schlüssel (min. 8) angeben.", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user