feat(workflow): introduce Event-First workflow with improved UX and test data seeding
- Replaced `VeranstalterAuswahlV2` with `VeranstaltungenUebersichtV2` for a direct entry point to event management. - Integrated onboarding directly into the event creation flow (`VeranstaltungKonfigV2`). - Added realistic test data (`StoreV2.seed()`) for instant workflow testing. - Updated initial navigation flow (`DesktopApp.kt`) to prioritize the event-first approach. - Enhanced screen and component interactions to streamline the user journey in offline-first mode. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
parent
683ef956fc
commit
1699c24875
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
type: Journal
|
||||||
|
status: ACTIVE
|
||||||
|
owner: Curator
|
||||||
|
last_update: 2026-03-31
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session Log: Event-First Workflow & UX-Polish (Initialer Schliff)
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Im Rahmen der MVP-Phase wurde der Fokus auf den "Event-First" Workflow gelegt. Ziel ist es, dass die App direkt mit der
|
||||||
|
Turnierverwaltung (Offline-First) startet, ohne den Nutzer durch ein separates Onboarding oder Login zu zwingen, solange
|
||||||
|
er lokal arbeitet.
|
||||||
|
|
||||||
|
## Durchgeführte Änderungen
|
||||||
|
|
||||||
|
### 1. Navigation & App-Start
|
||||||
|
|
||||||
|
- **Direkter Einstieg:** Die App startet nun direkt im Screen `AppScreen.Veranstaltungen`.
|
||||||
|
- **Anpassung DesktopApp.kt:** Das Login-Gate wurde so erweitert, dass alle für den Turnier-Workflow relevanten
|
||||||
|
Screens (Veranstaltungen, Veranstalter, Turniere) auch ohne Authentifizierung zugänglich sind.
|
||||||
|
|
||||||
|
### 2. Veranstaltungen-Übersicht (Gesamtliste)
|
||||||
|
|
||||||
|
- **Neuer Screen `VeranstaltungenUebersichtV2`:** Implementierung einer zentralen Übersicht, die alle im lokalen Store
|
||||||
|
vorhandenen Veranstaltungen über alle Veranstalter hinweg anzeigt.
|
||||||
|
- **Funktionalität:**
|
||||||
|
- Listendarstellung mit Titel, Verein, Datum und Status.
|
||||||
|
- Navigation zum "Cockpit" einer Veranstaltung (`VeranstaltungUebersicht`).
|
||||||
|
- Button zur Neuanlage einer Veranstaltung (leitet zur Veranstalter-Auswahl weiter).
|
||||||
|
|
||||||
|
### 3. Integriertes Onboarding
|
||||||
|
|
||||||
|
- **Wizard-Erweiterung:** Das Geräte-Onboarding (Name & Sicherheitsschlüssel) wurde direkt in den
|
||||||
|
`VeranstaltungKonfigV2`-Wizard integriert. Nutzer müssen die Hardware-Informationen erst angeben, wenn sie die erste
|
||||||
|
Veranstaltung anlegen wollen.
|
||||||
|
|
||||||
|
### 4. Testdaten (Seed)
|
||||||
|
|
||||||
|
- **StoreV2.seed():** Es wurden realistische Testdaten für "Neumarkt 2026" und "Linz 2026" inklusive zugehöriger
|
||||||
|
Turniere angelegt, um den Workflow sofort testbar zu machen.
|
||||||
|
|
||||||
|
## Betroffene Dateien
|
||||||
|
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt`
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt`
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt` (Neu:
|
||||||
|
`VeranstaltungenUebersichtV2`)
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Stores.kt` (Neu: `allEvents()`, `seed()`)
|
||||||
|
- `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt` (Aufruf `seed()`)
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
- [ ] Verifikation der Detail-Ansicht für Turniere.
|
||||||
|
- [ ] Implementierung der mDNS Discovery für die lokale Vernetzung.
|
||||||
|
- [ ] ADR für das PDF-Rendering entwerfen.
|
||||||
|
|
@ -37,10 +37,16 @@ fun DesktopApp() {
|
||||||
val authState by authTokenManager.authState.collectAsState()
|
val authState by authTokenManager.authState.collectAsState()
|
||||||
|
|
||||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
// Login-Gate: Nicht-authentifizierte Screens → Login, außer Onboarding ist erlaubt
|
||||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Onboarding) {
|
// Vision_03 Update: Wir starten direkt in der Veranstaltungs-Übersicht (Offline-First)
|
||||||
|
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.Veranstaltungen
|
||||||
|
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
|
||||||
|
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
|
||||||
|
&& currentScreen !is AppScreen.VeranstaltungUebersicht && currentScreen !is AppScreen.TurnierDetail
|
||||||
|
&& currentScreen !is AppScreen.TurnierNeu
|
||||||
|
) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wenn noch keine Authentifizierung vorhanden ist, zuerst Onboarding anzeigen
|
// Standard: Direkt zur Veranstaltungs-Übersicht (Offline-First-Modus)
|
||||||
nav.navigateToScreen(AppScreen.Onboarding)
|
nav.navigateToScreen(AppScreen.Veranstaltungen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.window.singleWindowApplication
|
import androidx.compose.ui.window.singleWindowApplication
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hot-Reload Preview Entry Point
|
* Hot-Reload Preview Entry Point
|
||||||
|
|
@ -32,7 +30,7 @@ private fun PreviewContent() {
|
||||||
// ReiterScreen(viewModel = ReiterViewModel())
|
// ReiterScreen(viewModel = ReiterViewModel())
|
||||||
|
|
||||||
// --- PFERDE ---
|
// --- PFERDE ---
|
||||||
PferdeScreen(viewModel = PferdeViewModel())
|
// PferdeScreen(viewModel = PferdeViewModel())
|
||||||
|
|
||||||
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
// ── Hier den gewünschten Screen eintragen ──────────────────────
|
||||||
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
// VeranstalterAuswahlScreen(onVeranstalterSelected = {}, onNeuerVeranstalter = {})
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ fun main() = application {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println("[DesktopApp] KOIN initialisiert")
|
println("[DesktopApp] KOIN initialisiert")
|
||||||
|
// Testdaten für Prototyp laden
|
||||||
|
at.mocode.desktop.v2.StoreV2.seed()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
println("[DesktopApp] Koin-Warnung: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
||||||
import at.mocode.turnier.feature.presentation.TurnierWizardV2
|
import at.mocode.turnier.feature.presentation.TurnierWizardV2
|
||||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
import at.mocode.veranstalter.feature.presentation.FakeVeranstalterStore
|
||||||
import at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore
|
import at.mocode.veranstalter.feature.presentation.FakeVeranstaltungStore
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterAuswahlV2
|
|
||||||
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
import at.mocode.veranstalter.feature.presentation.VeranstalterNeuScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||||
|
|
@ -301,19 +300,17 @@ private fun DesktopContentArea(
|
||||||
|
|
||||||
// Root-Screen: Leitet in V2-Fluss ab
|
// Root-Screen: Leitet in V2-Fluss ab
|
||||||
is AppScreen.Veranstaltungen -> {
|
is AppScreen.Veranstaltungen -> {
|
||||||
// Direkt zur Veranstalter-Auswahl V2
|
at.mocode.desktop.v2.VeranstaltungenUebersichtV2(
|
||||||
VeranstalterAuswahlV2(
|
onEventOpen = { vId, eId -> onNavigate(AppScreen.VeranstaltungUebersicht(vId, eId)) },
|
||||||
onZurueck = { /* bleibt root */ },
|
onNewEvent = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||||
onWeiter = { vId -> onNavigate(AppScreen.VeranstalterDetail(vId)) },
|
|
||||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahlV2(
|
is AppScreen.VeranstalterAuswahl -> at.mocode.desktop.v2.VeranstalterAuswahlV2(
|
||||||
onZurueck = { onNavigate(AppScreen.Veranstaltungen) },
|
onBack = { onNavigate(AppScreen.Veranstaltungen) },
|
||||||
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||||
onNeuerVeranstalter = { onNavigate(AppScreen.VeranstalterNeu) },
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
is AppScreen.VeranstalterNeu -> VeranstalterNeuScreen(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,60 @@ object StoreV2 {
|
||||||
|
|
||||||
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf()
|
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf()
|
||||||
|
|
||||||
|
fun seed() {
|
||||||
|
// Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun
|
||||||
|
if (veranstaltungen.isNotEmpty()) return
|
||||||
|
|
||||||
|
// 1. Neumarkt 2026 (ID 100)
|
||||||
|
val neumarktId = 100L
|
||||||
|
addEventFirst(
|
||||||
|
1, VeranstaltungV2(
|
||||||
|
id = neumarktId,
|
||||||
|
veranstalterId = 1,
|
||||||
|
titel = "Frühjahrsturnier Neumarkt/M. 2026",
|
||||||
|
datumVon = "2026-04-10",
|
||||||
|
datumBis = "2026-04-12",
|
||||||
|
status = "Nennungsphase"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Turniere für Neumarkt
|
||||||
|
TurnierStoreV2.add(
|
||||||
|
neumarktId,
|
||||||
|
TurnierV2(101, neumarktId, 26128, "CSN-C-NEU CSNP-C-NEU", "2026-04-10", "2026-04-12")
|
||||||
|
)
|
||||||
|
TurnierStoreV2.add(
|
||||||
|
neumarktId,
|
||||||
|
TurnierV2(102, neumarktId, 26129, "CDN-C-NEU CDNP-C-NEU", "2026-04-10", "2026-04-12")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Linz 2026 (ID 200)
|
||||||
|
val linzId = 200L
|
||||||
|
addEventFirst(
|
||||||
|
2, VeranstaltungV2(
|
||||||
|
id = linzId,
|
||||||
|
veranstalterId = 2,
|
||||||
|
titel = "Linzer Pferdefestival",
|
||||||
|
datumVon = "2026-05-20",
|
||||||
|
datumBis = "2026-05-24",
|
||||||
|
status = "In Vorbereitung"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TurnierStoreV2.add(linzId, TurnierV2(201, linzId, 26500, "CSN-B*", "2026-05-20", "2026-05-24"))
|
||||||
|
|
||||||
|
// 3. Ein historisches Event (ID 300)
|
||||||
|
addEventFirst(
|
||||||
|
1, VeranstaltungV2(
|
||||||
|
id = 300L,
|
||||||
|
veranstalterId = 1,
|
||||||
|
titel = "Herbst-Turnier 2025",
|
||||||
|
datumVon = "2025-09-15",
|
||||||
|
datumBis = "2025-09-17",
|
||||||
|
status = "Abgeschlossen"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> =
|
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> =
|
||||||
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
|
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
|
||||||
|
|
||||||
|
|
@ -39,4 +93,6 @@ object StoreV2 {
|
||||||
val idx = list.indexOfFirst { it.id == veranstaltungId }
|
val idx = list.indexOfFirst { it.id == veranstaltungId }
|
||||||
if (idx >= 0) list.removeAt(idx)
|
if (idx >= 0) list.removeAt(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun allEvents(): List<VeranstaltungV2> = veranstaltungen.values.flatten()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
|
@ -13,8 +15,71 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VeranstaltungenUebersichtV2(
|
||||||
|
onEventOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
|
||||||
|
onNewEvent: () -> Unit
|
||||||
|
) {
|
||||||
|
DesktopThemeV2 {
|
||||||
|
val events = remember { StoreV2.allEvents() }
|
||||||
|
val vereine = StoreV2.vereine
|
||||||
|
|
||||||
|
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("Alle Veranstaltungen", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Button(onClick = onNewEvent) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Neue Veranstaltung")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Keine Veranstaltungen gefunden.", color = Color.Gray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
items(events.sortedByDescending { it.datumVon }) { event ->
|
||||||
|
val verein = vereine.find { it.id == event.veranstalterId }
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().clickable { onEventOpen(event.veranstalterId, event.id) },
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(event.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
|
Text(
|
||||||
|
"${verein?.name ?: "Unbekannter Verein"} | ${event.datumVon} bis ${event.datumBis ?: ""}",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
event.status,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VeranstaltungKonfigV2(
|
fun VeranstaltungKonfigV2(
|
||||||
veranstalterId: Long,
|
veranstalterId: Long,
|
||||||
|
|
@ -22,34 +87,102 @@ fun VeranstaltungKonfigV2(
|
||||||
onSaved: (Long) -> Unit,
|
onSaved: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
DesktopThemeV2 {
|
DesktopThemeV2 {
|
||||||
|
var currentStep by remember { mutableStateOf(1) }
|
||||||
|
var geraetName by remember { mutableStateOf("") }
|
||||||
|
var securityKey by remember { mutableStateOf("") }
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Zurück",
|
contentDescription = "Zurück",
|
||||||
modifier = Modifier.clickable { onBack() })
|
modifier = Modifier.clickable { onBack() })
|
||||||
Text("Neue Veranstaltung", style = MaterialTheme.typography.titleLarge)
|
Text(
|
||||||
|
if (currentStep == 1) "Neue Veranstaltung: Geräteschutz" else "Neue Veranstaltung: Basisdaten",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var titel by remember { mutableStateOf("") }
|
if (currentStep == 1) {
|
||||||
var von by remember { mutableStateOf("") }
|
// --- STEP 1: Device Onboarding ---
|
||||||
var bis by remember { mutableStateOf("") }
|
Text(
|
||||||
|
"Bevor du eine Veranstaltung anlegst, musst du dieses Gerät benennen und einen lokalen Sicherheitsschlüssel festlegen.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
OutlinedTextField(value = titel, onValueChange = { titel = it }, label = { Text("Titel (Pflicht)") }, modifier = Modifier.fillMaxWidth())
|
OutlinedTextField(
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
value = geraetName,
|
||||||
OutlinedTextField(value = von, onValueChange = { von = it }, label = { Text("von (YYYY-MM-DD)") }, modifier = Modifier.weight(1f))
|
onValueChange = { geraetName = it },
|
||||||
OutlinedTextField(value = bis, onValueChange = { bis = it }, label = { Text("bis (YYYY-MM-DD)") }, modifier = Modifier.weight(1f))
|
label = { Text("Gerätename (z.B. Meldestelle-1)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = securityKey,
|
||||||
|
onValueChange = { securityKey = it },
|
||||||
|
label = { Text("Lokaler Sicherheitsschlüssel") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
visualTransformation = PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
|
||||||
|
val step1Enabled = geraetName.isNotBlank() && securityKey.length >= 8
|
||||||
|
Button(onClick = { currentStep = 2 }, enabled = step1Enabled) {
|
||||||
|
Text("Weiter zu den Veranstaltungsdaten")
|
||||||
|
}
|
||||||
|
if (securityKey.isNotEmpty() && securityKey.length < 8) {
|
||||||
|
Text(
|
||||||
|
"Der Schlüssel muss mindestens 8 Zeichen lang sein.",
|
||||||
|
color = Color(0xFFB00020),
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// --- STEP 2: Event Data ---
|
||||||
|
var titel by remember { mutableStateOf("") }
|
||||||
|
var von by remember { mutableStateOf("") }
|
||||||
|
var bis by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = titel,
|
||||||
|
onValueChange = { titel = it },
|
||||||
|
label = { Text("Titel (Pflicht)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = von,
|
||||||
|
onValueChange = { von = it },
|
||||||
|
label = { Text("von (YYYY-MM-DD)") },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = bis,
|
||||||
|
onValueChange = { bis = it },
|
||||||
|
label = { Text("bis (YYYY-MM-DD)") },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val validDates = von.isNotBlank() && (bis.isBlank() || bis >= von)
|
||||||
|
if (!validDates && von.isNotEmpty()) Text(
|
||||||
|
"bis-Datum darf nicht vor von-Datum liegen",
|
||||||
|
color = Color(0xFFB00020)
|
||||||
|
)
|
||||||
|
val enabled = titel.trim().isNotEmpty() && validDates
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedButton(onClick = { currentStep = 1 }) { Text("Zurück") }
|
||||||
|
Button(onClick = {
|
||||||
|
val id = System.currentTimeMillis()
|
||||||
|
StoreV2.addEventFirst(
|
||||||
|
veranstalterId,
|
||||||
|
VeranstaltungV2(id, veranstalterId, titel.trim(), von.trim(), bis.trim().ifBlank { null })
|
||||||
|
)
|
||||||
|
onSaved(id)
|
||||||
|
}, enabled = enabled) { Text("Veranstaltung anlegen") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val validDates = von.isNotBlank() && (bis.isBlank() || bis >= von)
|
|
||||||
if (!validDates) Text("bis-Datum darf nicht vor von-Datum liegen", color = Color(0xFFB00020))
|
|
||||||
val enabled = titel.trim().isNotEmpty() && validDates
|
|
||||||
|
|
||||||
Button(onClick = {
|
|
||||||
val id = System.currentTimeMillis()
|
|
||||||
StoreV2.addEventFirst(veranstalterId, VeranstaltungV2(id, veranstalterId, titel.trim(), von.trim(), bis.trim().ifBlank { null }))
|
|
||||||
onSaved(id)
|
|
||||||
}, enabled = enabled) { Text("Speichern") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user