refactor(desktop): V2-Suffixe entfernt und VeranstaltungKomponenten modularisiert

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-17 11:40:06 +02:00
parent 0128f98164
commit 3949ab21db
8 changed files with 696 additions and 512 deletions

View File

@ -0,0 +1,49 @@
# Session Journal: 2026-04-17 - Aufräumarbeiten & Konsolidierung
## 🎯 Ziele der Session
1. **V2-Cleanup:** Entfernung aller `V2`-Suffixe aus dem Codebase (Modelle, Stores, Wizards), um eine konsolidierte "
Source of Truth" zu schaffen.
2. **Refactoring:** Zerlegung der massiven `VeranstaltungKonfig`-Komponente in wartbare Teil-Module.
3. **Duplikat-Entfernung:** Zentralisierung von UI-Logik (DatePicker, Validierung) zur Reduzierung von Code-Duplikaten.
## 🛠️ Durchgeführte Änderungen
### 🧹 1. Konsolidierung der Benamung (V2-Entfernung)
* **Änderungen:**
* `VeranstaltungKonfigV2` -> `VeranstaltungKonfig`
* `VeranstaltungV2` -> `Veranstaltung`
* `TurnierV2` -> `Turnier`
* `StoreV2` -> `Store`
* `TurnierStoreV2` -> `TurnierStore`
* `TurnierWizardV2` -> `TurnierWizard`
* **Grund:** Umsetzung der Vereinbarung, nur noch eine "echte" Version zu pflegen und Altlasten aus Migrationsphasen zu
entfernen. Alle Referenzen im gesamten Projekt (`DesktopMainLayout.kt`, `ManagementScreens.kt`, `main.kt`) wurden
erfolgreich aktualisiert.
### 🏗️ 2. Refactoring `VeranstaltungScreens.kt`
* **Extraktion:** Die Wizard-Schritte wurden in eigenständige Composable-Funktionen ausgelagert:
* `Step1Veranstalter`: Auswahl aus ZNS/Lokal-Bestand.
* `Step2Basisdaten`: Titel, Zeitraum, Ort, Disziplinen.
* `Step3Details`: Logo, Sponsoren, Bewerbs-Management.
* **Zentralisierung:**
* Neue Komponente `AppDatePickerDialog` zur Vermeidung von dreifach redundantem Dialog-Code.
* Konsolidierte Validierungslogik für den Veranstaltungszeitraum.
### 🏷️ 3. Fehlerbehebung & Qualitätssicherung
* **Syntax-Fix:** Korrektur von Klammerfehlern, die während des Refactorings in der großen `VeranstaltungScreens.kt`
entstanden sind.
* **Linting:** Erfolgreiche Validierung der Dateien `VeranstaltungScreens.kt`, `Stores.kt` und `DesktopMainLayout.kt`.
## ✅ Ergebnis & Status
* Der Code ist nun wesentlich modularer und besser lesbar.
* Die Benamung ist konsistent ohne verwirrende Versions-Suffixe.
* Redundante Logik-Blöcke (besonders beim Datum-Handling) wurden eliminiert.
---
**🏗️ [Lead Architect]** & **🧹 [Curator]**
Datum: 17. April 2026 | Status: Abgeschlossen

View File

@ -50,11 +50,22 @@
* Anzeige der letzten Sync-Version (z.B. `ZNS: V12` oder `ZNS: Kein Sync`). * Anzeige der letzten Sync-Version (z.B. `ZNS: V12` oder `ZNS: Kein Sync`).
* Farbliche Kennzeichnung (Grün/Gelb/Rot) je nach Synchronisationsstand. * Farbliche Kennzeichnung (Grün/Gelb/Rot) je nach Synchronisationsstand.
### 🏗️ 5. Fachlich: Disziplinen & Bewerbe (Schritt 2 & 3)
* **Datei:** `frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/VeranstaltungScreens.kt`
* **Änderung:**
* **Schritt 2:** Felder für PLZ und Disziplin-Auswahl (Springen, Dressur, etc.) hinzugefügt.
* **Schritt 3:** Komplettes Bewerbs-Management implementiert. User können Prüfungsnummern, Klassen und Bezeichnungen
erfassen.
* **Validierung:** Der Wizard lässt sich erst finalisieren, wenn mindestens ein Bewerb angelegt wurde.
* **Grund:** Vorbereitung der Datenstruktur für den OEPS-Export und Verbesserung der fachlichen Abdeckung im Wizard.
## ✅ Ergebnis & Status ## ✅ Ergebnis & Status
* Das Consul-Dashboard sollte nun einen stabilen "Grün"-Status für den `masterdata-service` anzeigen. * Das Consul-Dashboard sollte nun einen stabilen "Grün"-Status für den `masterdata-service` anzeigen.
* Der Desktop-Wizard leitet den User fachlich korrekt durch die Turnier-Anlage. * Der Desktop-Wizard leitet den User fachlich korrekt durch die Turnier-Anlage.
* Der User hat jederzeit volle Transparenz über den Stand seiner lokalen ZNS-Daten. * Der User hat jederzeit volle Transparenz über den Stand seiner lokalen ZNS-Daten.
* Die Erfassung von Bewerben legt den Grundstein für die spätere Nennungs- und Ergebnisverwaltung.
--- ---
**🏗️ [Lead Architect]** & **🧹 [Curator]** **🏗️ [Lead Architect]** & **🧹 [Curator]**

View File

@ -12,13 +12,13 @@ import at.mocode.frontend.core.localdb.localDbModule
import at.mocode.frontend.core.network.networkModule import at.mocode.frontend.core.network.networkModule
import at.mocode.frontend.core.sync.di.syncModule import at.mocode.frontend.core.sync.di.syncModule
import at.mocode.frontend.features.billing.di.billingModule import at.mocode.frontend.features.billing.di.billingModule
import at.mocode.frontend.features.profile.di.profileModule
import at.mocode.frontend.features.verein.di.vereinFeatureModule
import at.mocode.frontend.features.nennung.di.nennungFeatureModule import at.mocode.frontend.features.nennung.di.nennungFeatureModule
import at.mocode.frontend.features.pferde.di.pferdeModule import at.mocode.frontend.features.pferde.di.pferdeModule
import at.mocode.frontend.features.profile.di.profileModule
import at.mocode.frontend.features.reiter.di.reiterModule import at.mocode.frontend.features.reiter.di.reiterModule
import at.mocode.turnier.feature.di.turnierFeatureModule import at.mocode.frontend.features.verein.di.vereinFeatureModule
import at.mocode.ping.feature.di.pingFeatureModule import at.mocode.ping.feature.di.pingFeatureModule
import at.mocode.turnier.feature.di.turnierFeatureModule
import at.mocode.zns.feature.di.znsImportModule import at.mocode.zns.feature.di.znsImportModule
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
@ -48,7 +48,7 @@ fun main() = application {
} }
println("[DesktopApp] KOIN initialisiert") println("[DesktopApp] KOIN initialisiert")
// Testdaten für Prototyp laden // Testdaten für Prototyp laden
at.mocode.desktop.v2.StoreV2.seed() at.mocode.desktop.v2.Store.seed()
} catch (e: Exception) { } catch (e: Exception) {
println("[DesktopApp] Koin-Warnung: ${e.message}") println("[DesktopApp] Koin-Warnung: ${e.message}")
} }

View File

@ -660,7 +660,7 @@ private fun DesktopContentArea(
is AppScreen.VeranstaltungKonfig -> { is AppScreen.VeranstaltungKonfig -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard // Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
at.mocode.desktop.v2.VeranstaltungKonfigV2( at.mocode.desktop.v2.VeranstaltungKonfig(
veranstalterId = vId, veranstalterId = vId,
onBack = onBack, onBack = onBack,
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) }, onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
@ -671,12 +671,12 @@ private fun DesktopContentArea(
is AppScreen.VeranstaltungProfil -> { is AppScreen.VeranstaltungProfil -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
if (at.mocode.desktop.v2.StoreV2.vereine.none { it.id == vId }) { if (at.mocode.desktop.v2.Store.vereine.none { it.id == vId }) {
InvalidContextNotice( InvalidContextNotice(
message = "Veranstalter (ID=$vId) nicht gefunden.", message = "Veranstalter (ID=$vId) nicht gefunden.",
onBack = onBack onBack = onBack
) )
} else if (at.mocode.desktop.v2.StoreV2.eventsFor(vId).none { it.id == evtId }) { } else if (at.mocode.desktop.v2.Store.eventsFor(vId).none { it.id == evtId }) {
InvalidContextNotice( InvalidContextNotice(
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.", message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
onBack = onBack onBack = onBack
@ -687,17 +687,17 @@ private fun DesktopContentArea(
veranstaltungId = evtId, veranstaltungId = evtId,
onBack = onBack, onBack = onBack,
onTurnierNeu = { onTurnierNeu = {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(vId).firstOrNull { it.id == evtId } val veranstaltung = at.mocode.desktop.v2.Store.eventsFor(vId).firstOrNull { it.id == evtId }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(evtId) val list = at.mocode.desktop.v2.TurnierStore.list(evtId)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2( val draft = at.mocode.desktop.v2.Turnier(
id = newId, id = newId,
veranstaltungId = evtId, veranstaltungId = evtId,
turnierNr = 0, turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "", datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis, datumBis = veranstaltung?.datumBis,
) )
at.mocode.desktop.v2.TurnierStoreV2.add(evtId, draft) at.mocode.desktop.v2.TurnierStore.add(evtId, draft)
onNavigate(AppScreen.TurnierDetail(evtId, newId)) onNavigate(AppScreen.TurnierDetail(evtId, newId))
}, },
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
@ -711,21 +711,21 @@ private fun DesktopContentArea(
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
onBack = onBack, onBack = onBack,
onTurnierNeu = { onTurnierNeu = {
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv -> val v = at.mocode.desktop.v2.Store.vereine.firstOrNull { vv ->
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id } at.mocode.desktop.v2.Store.eventsFor(vv.id).any { it.id == currentScreen.id }
} }
val veranstaltung = val veranstaltung =
v?.let { at.mocode.desktop.v2.StoreV2.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } } v?.let { at.mocode.desktop.v2.Store.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(currentScreen.id) val list = at.mocode.desktop.v2.TurnierStore.list(currentScreen.id)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2( val draft = at.mocode.desktop.v2.Turnier(
id = newId, id = newId,
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
turnierNr = 0, turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "", datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis, datumBis = veranstaltung?.datumBis,
) )
at.mocode.desktop.v2.TurnierStoreV2.add(currentScreen.id, draft) at.mocode.desktop.v2.TurnierStore.add(currentScreen.id, draft)
onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId)) onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId))
}, },
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) }, onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
@ -739,8 +739,8 @@ private fun DesktopContentArea(
// Turnier-Screens // Turnier-Screens
is AppScreen.TurnierDetail -> { is AppScreen.TurnierDetail -> {
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
val parent = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { v -> val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v ->
at.mocode.desktop.v2.StoreV2.eventsFor(v.id).any { it.id == evtId } at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId }
} }
if (parent == null) { if (parent == null) {
InvalidContextNotice( InvalidContextNotice(
@ -748,7 +748,7 @@ private fun DesktopContentArea(
onBack = onBack onBack = onBack
) )
} else { } else {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(parent.id).firstOrNull { it.id == evtId } val veranstaltung = at.mocode.desktop.v2.Store.eventsFor(parent.id).firstOrNull { it.id == evtId }
val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: "" val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: ""
val bundesland = mapOepsToBundesland(blCode) val bundesland = mapOepsToBundesland(blCode)
TurnierDetailScreen( TurnierDetailScreen(
@ -769,8 +769,8 @@ private fun DesktopContentArea(
is AppScreen.TurnierNeu -> { is AppScreen.TurnierNeu -> {
val evtId = currentScreen.veranstaltungId val evtId = currentScreen.veranstaltungId
// V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert // V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert
val parent = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { v -> val parent = at.mocode.desktop.v2.Store.vereine.firstOrNull { v ->
at.mocode.desktop.v2.StoreV2.eventsFor(v.id).any { it.id == evtId } at.mocode.desktop.v2.Store.eventsFor(v.id).any { it.id == evtId }
} }
if (parent == null) { if (parent == null) {
InvalidContextNotice( InvalidContextNotice(
@ -778,7 +778,7 @@ private fun DesktopContentArea(
onBack = onBack onBack = onBack
) )
} else { } else {
at.mocode.desktop.v2.TurnierWizardV2( at.mocode.desktop.v2.TurnierWizard(
veranstalterId = parent.id, veranstalterId = parent.id,
veranstaltungId = evtId, veranstaltungId = evtId,
onBack = onBack, onBack = onBack,

View File

@ -6,9 +6,11 @@ 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.filled.* import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -140,7 +142,7 @@ data class TableColumn<T>(
@Composable @Composable
fun PferdeVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { fun PferdeVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val pferde = StoreV2.pferde val pferde = Store.pferde
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) pferde else pferde.filter { val filteredItems = if (filter.isEmpty()) pferde else pferde.filter {
it.name.contains(filter, ignoreCase = true) || it.feiId?.contains(filter, ignoreCase = true) == true it.name.contains(filter, ignoreCase = true) || it.feiId?.contains(filter, ignoreCase = true) == true
@ -162,14 +164,14 @@ fun PferdeVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
onBack = onBack, onBack = onBack,
onNew = { /* CRUD Logik */ }, onNew = { /* CRUD Logik */ },
onEdit = { onEdit(it.id) }, onEdit = { onEdit(it.id) },
onDelete = { StoreV2.pferde.remove(it) }, onDelete = { Store.pferde.remove(it) },
onSearch = { filter = it } onSearch = { filter = it }
) )
} }
@Composable @Composable
fun ReiterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { fun ReiterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val reiter = StoreV2.reiter val reiter = Store.reiter
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) reiter else reiter.filter { val filteredItems = if (filter.isEmpty()) reiter else reiter.filter {
it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains( it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(
@ -192,14 +194,14 @@ fun ReiterVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
onBack = onBack, onBack = onBack,
onNew = { }, onNew = { },
onEdit = { onEdit(it.id) }, onEdit = { onEdit(it.id) },
onDelete = { StoreV2.reiter.remove(it) }, onDelete = { Store.reiter.remove(it) },
onSearch = { filter = it } onSearch = { filter = it }
) )
} }
@Composable @Composable
fun VereinVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { fun VereinVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val vereine = StoreV2.vereine val vereine = Store.vereine
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {
it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true) it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true)
@ -218,14 +220,14 @@ fun VereinVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
onBack = onBack, onBack = onBack,
onNew = { }, onNew = { },
onEdit = { onEdit(it.id) }, onEdit = { onEdit(it.id) },
onDelete = { StoreV2.vereine.remove(it) }, onDelete = { Store.vereine.remove(it) },
onSearch = { filter = it } onSearch = { filter = it }
) )
} }
@Composable @Composable
fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
val funktionaere = StoreV2.funktionaere val funktionaere = Store.funktionaere
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) funktionaere else funktionaere.filter { val filteredItems = if (filter.isEmpty()) funktionaere else funktionaere.filter {
it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(filter, ignoreCase = true) it.vorname.contains(filter, ignoreCase = true) || it.nachname.contains(filter, ignoreCase = true)
@ -244,7 +246,7 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
onBack = onBack, onBack = onBack,
onNew = { }, onNew = { },
onEdit = { onEdit(it.id) }, onEdit = { onEdit(it.id) },
onDelete = { StoreV2.funktionaere.remove(it) }, onDelete = { Store.funktionaere.remove(it) },
onSearch = { filter = it } onSearch = { filter = it }
) )
} }
@ -253,7 +255,7 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) { fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) {
// Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten
// Wir nutzen hier die 'vereine' Liste aus dem Store. // Wir nutzen hier die 'vereine' Liste aus dem Store.
val vereine = StoreV2.vereine val vereine = Store.vereine
var filter by remember { mutableStateOf("") } var filter by remember { mutableStateOf("") }
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {
it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true) it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true)

View File

@ -431,7 +431,7 @@ fun OnboardingScreenPreview() {
@Composable @Composable
fun PferdProfilV2(id: Long, onBack: () -> Unit) { fun PferdProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 { DesktopThemeV2 {
val pferd = remember(id) { StoreV2.pferde.firstOrNull { it.id == id } } val pferd = remember(id) { Store.pferde.firstOrNull { it.id == id } }
if (pferd == null) { if (pferd == null) {
Text("Pferd nicht gefunden"); return@DesktopThemeV2 Text("Pferd nicht gefunden"); return@DesktopThemeV2
} }
@ -506,7 +506,7 @@ fun PferdProfilV2(id: Long, onBack: () -> Unit) {
@Composable @Composable
fun ReiterProfilV2(id: Long, onBack: () -> Unit) { fun ReiterProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 { DesktopThemeV2 {
val r = remember(id) { StoreV2.reiter.firstOrNull { it.id == id } } val r = remember(id) { Store.reiter.firstOrNull { it.id == id } }
if (r == null) { if (r == null) {
Text("Reiter nicht gefunden"); return@DesktopThemeV2 Text("Reiter nicht gefunden"); return@DesktopThemeV2
} }
@ -590,7 +590,7 @@ fun ReiterProfilV2(id: Long, onBack: () -> Unit) {
@Composable @Composable
fun VereinProfilV2(id: Long, onBack: () -> Unit) { fun VereinProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 { DesktopThemeV2 {
val v = remember(id) { StoreV2.vereine.firstOrNull { it.id == id } } val v = remember(id) { Store.vereine.firstOrNull { it.id == id } }
if (v == null) { if (v == null) {
Text("Verein nicht gefunden"); return@DesktopThemeV2 Text("Verein nicht gefunden"); return@DesktopThemeV2
} }
@ -681,7 +681,7 @@ fun VereinProfilV2(id: Long, onBack: () -> Unit) {
@Composable @Composable
fun FunktionaerProfilV2(id: Long, onBack: () -> Unit) { fun FunktionaerProfilV2(id: Long, onBack: () -> Unit) {
DesktopThemeV2 { DesktopThemeV2 {
val f = remember(id) { StoreV2.funktionaere.firstOrNull { it.id == id } } val f = remember(id) { Store.funktionaere.firstOrNull { it.id == id } }
if (f == null) { if (f == null) {
Text("Funktionär nicht gefunden"); return@DesktopThemeV2 Text("Funktionär nicht gefunden"); return@DesktopThemeV2
} }
@ -782,7 +782,7 @@ fun VeranstalterAuswahlV2(
var selectedId by remember { mutableStateOf<Long?>(null) } var selectedId by remember { mutableStateOf<Long?>(null) }
LazyColumn(Modifier.fillMaxSize()) { LazyColumn(Modifier.fillMaxSize()) {
items(StoreV2.vereine) { v -> items(Store.vereine) { v ->
val sel = selectedId == v.id val sel = selectedId == v.id
Card( Card(
modifier = Modifier modifier = Modifier
@ -822,14 +822,14 @@ fun VeranstalterDetailV2(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück", contentDescription = "Zurück",
modifier = Modifier.clickable { onBack() }) modifier = Modifier.clickable { onBack() })
val verein = StoreV2.vereine.firstOrNull { it.id == veranstalterId } val verein = Store.vereine.firstOrNull { it.id == veranstalterId }
Text(verein?.name ?: "Veranstalter", style = MaterialTheme.typography.titleLarge) Text(verein?.name ?: "Veranstalter", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") } Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
} }
// Veranstalter Vorschau-Karte mit Bearbeiten-Dialog // Veranstalter Vorschau-Karte mit Bearbeiten-Dialog
val verein = remember(veranstalterId) { StoreV2.vereine.firstOrNull { it.id == veranstalterId } } val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }
if (verein != null) { if (verein != null) {
var editOpen by remember { mutableStateOf(false) } var editOpen by remember { mutableStateOf(false) }
Card(Modifier.fillMaxWidth()) { Card(Modifier.fillMaxWidth()) {
@ -953,7 +953,7 @@ fun VeranstalterDetailV2(
} }
} }
val events = StoreV2.eventsFor(veranstalterId) val events = Store.eventsFor(veranstalterId)
// Filter-/Suchmaske // Filter-/Suchmaske
var search by remember { mutableStateOf("") } var search by remember { mutableStateOf("") }
OutlinedTextField( OutlinedTextField(
@ -992,7 +992,7 @@ fun VeranstalterDetailV2(
onDismissRequest = { confirm = false }, onDismissRequest = { confirm = false },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
StoreV2.removeEvent(veranstalterId, evt.id) Store.removeEvent(veranstalterId, evt.id)
confirm = false confirm = false
}) { Text("Löschen") } }) { Text("Löschen") }
}, },

View File

@ -71,7 +71,7 @@ data class Funktionaer(
var istAktiv: Boolean = true, var istAktiv: Boolean = true,
) )
data class VeranstaltungV2( data class Veranstaltung(
val id: Long, val id: Long,
var veranstalterId: Long, var veranstalterId: Long,
var titel: String, var titel: String,
@ -85,7 +85,7 @@ data class VeranstaltungV2(
var sponsoren: SnapshotStateList<String> = mutableStateListOf(), var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
) )
object StoreV2 { object Store {
val pferde: SnapshotStateList<Pferd> = mutableStateListOf( val pferde: SnapshotStateList<Pferd> = mutableStateListOf(
Pferd( Pferd(
id = 1, id = 1,
@ -268,7 +268,7 @@ object StoreV2 {
return id return id
} }
private val veranstaltungen: MutableMap<Long, SnapshotStateList<VeranstaltungV2>> = mutableMapOf() private val veranstaltungen: MutableMap<Long, SnapshotStateList<Veranstaltung>> = mutableMapOf()
fun seed() { fun seed() {
// Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun // Falls bereits Daten da sind (außer den statischen Vereinen), nichts tun
@ -277,7 +277,7 @@ object StoreV2 {
// 1. Neumarkt April 2026 (ID 100) // 1. Neumarkt April 2026 (ID 100)
val neumarktId = 100L val neumarktId = 100L
addEventFirst( addEventFirst(
1, VeranstaltungV2( 1, Veranstaltung(
id = neumarktId, id = neumarktId,
veranstalterId = 1, veranstalterId = 1,
titel = "CSN-B* Neumarkt am Wallersee", titel = "CSN-B* Neumarkt am Wallersee",
@ -289,17 +289,17 @@ object StoreV2 {
) )
) )
TurnierStoreV2.add( TurnierStore.add(
neumarktId, neumarktId,
TurnierV2(101, neumarktId, 26128, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply { Turnier(101, neumarktId, 26128, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply {
titel = "Springturnier Neumarkt" titel = "Springturnier Neumarkt"
kategorie.add("CSN-B*") kategorie.add("CSN-B*")
kategorie.add("CSNP-B") kategorie.add("CSNP-B")
} }
) )
TurnierStoreV2.add( TurnierStore.add(
neumarktId, neumarktId,
TurnierV2(102, neumarktId, 26129, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply { Turnier(102, neumarktId, 26129, datumVon = "2026-04-24", datumBis = "2026-04-26", znsDataLoaded = true).apply {
titel = "Dressurturnier Neumarkt" titel = "Dressurturnier Neumarkt"
kategorie.add("CDN-B") kategorie.add("CDN-B")
kategorie.add("CDNP-B") kategorie.add("CDNP-B")
@ -309,7 +309,7 @@ object StoreV2 {
// 2. Linz 2026 (ID 200) // 2. Linz 2026 (ID 200)
val linzId = 200L val linzId = 200L
addEventFirst( addEventFirst(
2, VeranstaltungV2( 2, Veranstaltung(
id = linzId, id = linzId,
veranstalterId = 2, veranstalterId = 2,
titel = "Linzer Pferdefestival", titel = "Linzer Pferdefestival",
@ -319,15 +319,15 @@ object StoreV2 {
beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark." beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark."
) )
) )
TurnierStoreV2.add( TurnierStore.add(
linzId, linzId,
TurnierV2(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply { Turnier(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply {
kategorie.add("CSN-B*") kategorie.add("CSN-B*")
}) })
// 3. Ein historisches Event (ID 300) // 3. Ein historisches Event (ID 300)
addEventFirst( addEventFirst(
1, VeranstaltungV2( 1, Veranstaltung(
id = 300L, id = 300L,
veranstalterId = 1, veranstalterId = 1,
titel = "Herbst-Turnier 2025", titel = "Herbst-Turnier 2025",
@ -338,10 +338,10 @@ object StoreV2 {
) )
} }
fun eventsFor(vereinId: Long): SnapshotStateList<VeranstaltungV2> = fun eventsFor(vereinId: Long): SnapshotStateList<Veranstaltung> =
veranstaltungen.getOrPut(vereinId) { mutableStateListOf() } veranstaltungen.getOrPut(vereinId) { mutableStateListOf() }
fun addEventFirst(vereinId: Long, v: VeranstaltungV2) { fun addEventFirst(vereinId: Long, v: Veranstaltung) {
eventsFor(vereinId).add(0, v) eventsFor(vereinId).add(0, v)
} }
@ -351,5 +351,5 @@ object StoreV2 {
if (idx >= 0) list.removeAt(idx) if (idx >= 0) list.removeAt(idx)
} }
fun allEvents(): List<VeranstaltungV2> = veranstaltungen.values.flatten() fun allEvents(): List<Veranstaltung> = veranstaltungen.values.flatten()
} }

View File

@ -44,8 +44,8 @@ fun VeranstaltungVerwaltung(
onNavigateToZnsImport: () -> Unit onNavigateToZnsImport: () -> Unit
) { ) {
DesktopThemeV2 { DesktopThemeV2 {
val allVeranstaltungen = remember { StoreV2.allEvents() } val allVeranstaltungen = remember { Store.allEvents() }
val vereine = StoreV2.vereine val vereine = Store.vereine
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
var selectedStatus by remember { mutableStateOf<String?>(null) } var selectedStatus by remember { mutableStateOf<String?>(null) }
@ -223,7 +223,7 @@ fun VeranstalterAnlegenWizard(
val results = remember(searchQuery) { val results = remember(searchQuery) {
if (searchQuery.length < 2) emptyList() if (searchQuery.length < 2) emptyList()
else StoreV2.oepsStammdaten.filter { else Store.oepsStammdaten.filter {
it.name.contains(searchQuery, ignoreCase = true) || it.name.contains(searchQuery, ignoreCase = true) ||
(it.ort?.contains(searchQuery, ignoreCase = true) ?: false) || (it.ort?.contains(searchQuery, ignoreCase = true) ?: false) ||
it.oepsNummer.contains(searchQuery, ignoreCase = true) it.oepsNummer.contains(searchQuery, ignoreCase = true)
@ -295,7 +295,7 @@ fun VeranstalterAnlegenWizard(
TextButton(onClick = { step = 1 }) { Text("Zurück zur Suche") } TextButton(onClick = { step = 1 }) { Text("Zurück zur Suche") }
Button( Button(
onClick = { onClick = {
val newId = StoreV2.addVerein(name, oeps, ort) val newId = Store.addVerein(name, oeps, ort)
onVereinCreated(newId) onVereinCreated(newId)
}, },
enabled = name.isNotBlank() && ort.isNotBlank() enabled = name.isNotBlank() && ort.isNotBlank()
@ -309,129 +309,55 @@ fun VeranstalterAnlegenWizard(
} }
} }
@OptIn(ExperimentalMaterial3Api::class) class BewerbData(
@Composable val nummer: String,
fun VeranstaltungKonfigV2( val abteilung: String?,
veranstalterId: Long = 0, val klasse: String,
onBack: () -> Unit, val disziplin: String,
onSaved: (Long, Long) -> Unit, // eventId, veranstalterId val bezeichnung: String
onVeranstalterCreated: (Long) -> Unit = {}, // Neuer Flow: nach Vereinsanlage ins Profil )
) {
val znsImporter: ZnsImportProvider = koinInject()
val znsState = znsImporter.state
DesktopThemeV2 { fun Long?.toLocalDate(): LocalDate? {
var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) }
// Step 1: Veranstalterwahl
var selectedVereinId by remember { mutableStateOf(veranstalterId) }
var showVereinNeu by remember { mutableStateOf(false) }
// Step 2: Basisdaten
var titel by remember { mutableStateOf("") }
var untertitel by remember { mutableStateOf("") }
var von by remember { mutableStateOf("") }
var bis by remember { mutableStateOf("") }
var ort by remember { mutableStateOf("") }
var showDatePickerVon by remember { mutableStateOf(false) }
var showDatePickerBis by remember { mutableStateOf(false) }
// Step 3: Zusatzdaten
var logoUrl by remember { mutableStateOf("") }
var sponsorenText by remember { mutableStateOf("") } // Kommagetrennte Liste
val dateFormatter = remember { DateTimeFormatter.ISO_LOCAL_DATE }
fun Long?.toLocalDate(): LocalDate? {
if (this == null) return null if (this == null) return null
return Instant.ofEpochMilli(this).atZone(ZoneId.systemDefault()).toLocalDate() return Instant.ofEpochMilli(this).atZone(ZoneId.systemDefault()).toLocalDate()
} }
if (showDatePickerVon) { @OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppDatePickerDialog(
onDismiss: () -> Unit,
onDateSelected: (LocalDate) -> Unit,
) {
val datePickerState = rememberDatePickerState() val datePickerState = rememberDatePickerState()
DatePickerDialog( DatePickerDialog(
onDismissRequest = { showDatePickerVon = false }, onDismissRequest = onDismiss,
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
datePickerState.selectedDateMillis.toLocalDate()?.let { datePickerState.selectedDateMillis.toLocalDate()?.let {
von = it.format(dateFormatter) onDateSelected(it)
} }
showDatePickerVon = false onDismiss()
}) { Text("OK") } }) { Text("OK") }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showDatePickerVon = false }) { Text("Abbrechen") } TextButton(onClick = onDismiss) { Text("Abbrechen") }
} }
) { ) {
DatePicker(state = datePickerState) DatePicker(state = datePickerState)
} }
} }
if (showDatePickerBis) { @OptIn(ExperimentalMaterial3Api::class)
val datePickerState = rememberDatePickerState() @Composable
DatePickerDialog( fun Step1Veranstalter(
onDismissRequest = { showDatePickerBis = false }, znsState: at.mocode.frontend.core.domain.zns.ZnsImportState,
confirmButton = { znsImporter: ZnsImportProvider,
TextButton(onClick = { selectedVereinId: Long,
datePickerState.selectedDateMillis.toLocalDate()?.let { onVereinSelected: (Long) -> Unit,
bis = it.format(dateFormatter) onVeranstalterCreated: (Long) -> Unit,
} ) {
showDatePickerBis = false var showVereinNeu by remember { mutableStateOf(false) }
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showDatePickerBis = false }) { Text("Abbrechen") }
}
) {
DatePicker(state = datePickerState)
}
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// Header & Navigation
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
IconButton(onClick = {
if (currentStep > 1) {
// Wenn wir aus einem konkreten Veranstalter kommen (id > 0),
// gehen wir bei Zurück direkt ins Profil statt auf Schritt 1.
if (veranstalterId != 0L) {
onBack()
} else {
currentStep--
}
} else {
onBack()
}
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Column {
Text("Neue Veranstaltung anlegen", style = MaterialTheme.typography.headlineSmall)
Text(
when (currentStep) {
1 -> "Schritt 1: Veranstalter auswählen"
2 -> "Schritt 2: Basisdaten der Veranstaltung"
3 -> "Schritt 3: Details & Sponsoren"
else -> ""
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
LinearProgressIndicator(
progress = { currentStep / 3f },
modifier = Modifier.fillMaxWidth().height(4.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceVariant
)
Box(Modifier.weight(1f).fillMaxWidth()) {
when (currentStep) {
1 -> {
// --- SCHRITT 1: ZNS-First Daten-Akquise ---
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text( Text(
"Daten-Akquise & Veranstalter", "Daten-Akquise & Veranstalter",
@ -439,7 +365,6 @@ fun VeranstaltungKonfigV2(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
// 1. ZNS Import Bereich (Prominent)
ZnsImportWizardSection( ZnsImportWizardSection(
state = znsState, state = znsState,
onFileSelect = { path -> znsImporter.onFileSelected(path) }, onFileSelect = { path -> znsImporter.onFileSelected(path) },
@ -449,7 +374,6 @@ fun VeranstaltungKonfigV2(
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
// 2. Cloud Sync (Neu gemäß User-Wunsch)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -459,8 +383,8 @@ fun VeranstaltungKonfigV2(
onClick = { onClick = {
znsImporter.syncFromCloud { remoteList -> znsImporter.syncFromCloud { remoteList ->
remoteList.forEach { remote -> remoteList.forEach { remote ->
StoreV2.vereine.find { it.oepsNummer == remote.oepsNummer } Store.vereine.find { it.oepsNummer == remote.oepsNummer }
?: StoreV2.addVerein(remote.name, remote.oepsNummer, remote.ort ?: "") ?: Store.addVerein(remote.name, remote.oepsNummer, remote.ort ?: "")
} }
} }
}, },
@ -498,11 +422,10 @@ fun VeranstaltungKonfigV2(
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
// 3. Bestehende Veranstalter (Kompakt)
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
var search by remember { mutableStateOf("") } var search by remember { mutableStateOf("") }
val filteredVereine = remember(search) { val filteredVereine = remember(search) {
StoreV2.vereine.filter { Store.vereine.filter {
it.name.contains(search, ignoreCase = true) || (it.ort?.contains(search, ignoreCase = true) it.name.contains(search, ignoreCase = true) || (it.ort?.contains(search, ignoreCase = true)
?: false) ?: false)
} }
@ -526,7 +449,7 @@ fun VeranstaltungKonfigV2(
items(filteredVereine) { verein -> items(filteredVereine) { verein ->
val isSelected = selectedVereinId == verein.id val isSelected = selectedVereinId == verein.id
Surface( Surface(
onClick = { selectedVereinId = verein.id }, onClick = { onVereinSelected(verein.id) },
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
border = if (isSelected) null else androidx.compose.foundation.BorderStroke( border = if (isSelected) null else androidx.compose.foundation.BorderStroke(
@ -555,7 +478,6 @@ fun VeranstaltungKonfigV2(
} }
} }
// 3. Manueller Button für neuen Veranstalter
OutlinedButton( OutlinedButton(
onClick = { showVereinNeu = true }, onClick = { showVereinNeu = true },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp), modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
@ -578,8 +500,7 @@ fun VeranstaltungKonfigV2(
onCancel = { showVereinNeu = false }, onCancel = { showVereinNeu = false },
onVereinCreated = { newId -> onVereinCreated = { newId ->
showVereinNeu = false showVereinNeu = false
selectedVereinId = newId onVeranstalterCreated(newId)
currentStep = 2 // Direkt zum nächsten Schritt
} }
) )
} }
@ -588,22 +509,48 @@ fun VeranstaltungKonfigV2(
) )
} }
} }
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun Step2Basisdaten(
titel: String, onTitelChange: (String) -> Unit,
untertitel: String, onUntertitelChange: (String) -> Unit,
von: String, onVonChange: (String) -> Unit,
bis: String, onBisChange: (String) -> Unit,
ort: String, onOrtChange: (String) -> Unit,
plz: String, onPlzChange: (String) -> Unit,
selectedDisziplinen: Set<String>, onDisziplinenChange: (Set<String>) -> Unit,
dateFormatter: DateTimeFormatter
) {
var showDatePickerVon by remember { mutableStateOf(false) }
var showDatePickerBis by remember { mutableStateOf(false) }
if (showDatePickerVon) {
AppDatePickerDialog(
onDismiss = { showDatePickerVon = false },
onDateSelected = { onVonChange(it.format(dateFormatter)) }
)
}
if (showDatePickerBis) {
AppDatePickerDialog(
onDismiss = { showDatePickerBis = false },
onDateSelected = { onBisChange(it.format(dateFormatter)) }
)
} }
2 -> {
// --- SCHRITT 2: Basisdaten ---
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text("Allgemeine Informationen", style = MaterialTheme.typography.titleMedium) Text("Allgemeine Informationen", style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = titel, value = titel,
onValueChange = { titel = it }, onValueChange = onTitelChange,
label = { Text("Titel der Veranstaltung (z.B. Pfingstturnier 2026)") }, label = { Text("Titel der Veranstaltung (z.B. Pfingstturnier 2026)") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
value = untertitel, value = untertitel,
onValueChange = { untertitel = it }, onValueChange = onUntertitelChange,
label = { Text("Untertitel / Slogan (optional)") }, label = { Text("Untertitel / Slogan (optional)") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -618,22 +565,17 @@ fun VeranstaltungKonfigV2(
} catch (_: Exception) { } catch (_: Exception) {
null null
} }
val today = LocalDate.now() val isStartInPast = dateVon != null && dateVon.isBefore(LocalDate.now())
val isStartInPast = dateVon != null && dateVon.isBefore(today)
val daysBetween = if (dateVon != null && dateBis != null) { val daysBetween = if (dateVon != null && dateBis != null) {
java.time.temporal.ChronoUnit.DAYS.between(dateVon, dateBis) + 1 java.time.temporal.ChronoUnit.DAYS.between(dateVon, dateBis) + 1
} else null } else null
val isOetoConform = daysBetween == null || daysBetween <= 2 val isOetoConform = daysBetween == null || daysBetween <= 2
val isDateRangeInvalid = (dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast
val isDateRangeInvalid =
(dateVon != null && dateBis != null && dateBis.isBefore(dateVon)) || isStartInPast
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField( OutlinedTextField(
value = von, value = von,
onValueChange = { /* Schreibgeschützt, via Picker */ }, onValueChange = { },
label = { Text("Datum von") }, label = { Text("Datum von") },
modifier = Modifier.weight(1f).clickable { showDatePickerVon = true }, modifier = Modifier.weight(1f).clickable { showDatePickerVon = true },
enabled = false, enabled = false,
@ -642,23 +584,14 @@ fun VeranstaltungKonfigV2(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledBorderColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline, disabledBorderColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
disabledLabelColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, disabledLabelColor = if (isStartInPast) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
), ),
trailingIcon = { trailingIcon = {
IconButton(onClick = { showDatePickerVon = true }) { IconButton(onClick = { showDatePickerVon = true }) { Icon(Icons.Default.DateRange, null) }
Icon(Icons.Default.DateRange, contentDescription = "Datum wählen")
}
},
supportingText = {
if (isStartInPast) {
Text("Startdatum darf nicht in der Vergangenheit liegen.")
}
} }
) )
OutlinedTextField( OutlinedTextField(
value = bis, value = bis,
onValueChange = { /* Schreibgeschützt, via Picker */ }, onValueChange = { },
label = { Text("Datum bis") }, label = { Text("Datum bis") },
modifier = Modifier.weight(1f).clickable { showDatePickerBis = true }, modifier = Modifier.weight(1f).clickable { showDatePickerBis = true },
enabled = false, enabled = false,
@ -667,58 +600,98 @@ fun VeranstaltungKonfigV2(
disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledBorderColor = if (isDateRangeInvalid) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline, disabledBorderColor = if (isDateRangeInvalid) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline,
disabledLabelColor = if (isDateRangeInvalid) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, disabledLabelColor = if (isDateRangeInvalid) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant
), ),
trailingIcon = { trailingIcon = {
IconButton(onClick = { showDatePickerBis = true }) { IconButton(onClick = { showDatePickerBis = true }) { Icon(Icons.Default.DateRange, null) }
Icon(Icons.Default.DateRange, contentDescription = "Datum wählen")
} }
},
supportingText = {
Column {
if (isDateRangeInvalid) {
Text("Enddatum darf nicht vor dem Startdatum liegen.")
}
if (isOetoConform.not()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary
) )
}
if (isStartInPast || isDateRangeInvalid || isOetoConform.not()) {
Column {
if (isStartInPast) Text(
"Startdatum darf nicht in der Vergangenheit liegen.",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall
)
if (isDateRangeInvalid) Text(
"Enddatum darf nicht vor dem Startdatum liegen.",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall
)
if (isOetoConform.not()) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(Icons.Default.Info, null, modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.primary)
Text( Text(
"Hinweis: Gemäß ÖTO sind C-Turniere auf 2 Tage begrenzt.", "Hinweis: Gemäß ÖTO sind C-Turniere auf 2 Tage begrenzt.",
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelSmall
) )
} }
} }
} }
} }
)
}
OutlinedTextField( OutlinedTextField(
value = ort, value = ort,
onValueChange = { ort = it }, onValueChange = onOrtChange,
label = { Text("Austragungsort (falls abweichend vom Vereinssitz)") }, label = { Text("Austragungsort (Name der Anlage / Ort)") },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = plz,
onValueChange = { if (it.length <= 4 && it.all { char -> char.isDigit() }) onPlzChange(it) },
label = { Text("PLZ") },
modifier = Modifier.width(100.dp),
singleLine = true
)
OutlinedTextField(
value = ort,
onValueChange = onOrtChange,
label = { Text("Ort") },
modifier = Modifier.weight(1f),
singleLine = true
)
}
Text("Disziplinen", style = MaterialTheme.typography.titleSmall)
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val disziplinen = listOf("Springen", "Dressur", "Vielseitigkeit", "Fahren", "Voltigieren", "Reining")
disziplinen.forEach { d ->
FilterChip(
selected = d in selectedDisziplinen,
onClick = {
onDisziplinenChange(if (d in selectedDisziplinen) selectedDisziplinen - d else selectedDisziplinen + d)
},
label = { Text(d) },
leadingIcon = if (d in selectedDisziplinen) {
{ Icon(Icons.Default.Check, null, modifier = Modifier.size(16.dp)) }
} else null
)
} }
} }
}
}
3 -> { @OptIn(ExperimentalMaterial3Api::class)
// --- SCHRITT 3: Details & Sponsoren --- @Composable
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { fun Step3Details(
logoUrl: String, onLogoUrlChange: (String) -> Unit,
sponsorenText: String, onSponsorenTextChange: (String) -> Unit,
bewerbe: SnapshotStateList<BewerbData>,
selectedDisziplinen: Set<String>
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
Text("Branding & Partner", style = MaterialTheme.typography.titleMedium) Text("Branding & Partner", style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = logoUrl, value = logoUrl,
onValueChange = { logoUrl = it }, onValueChange = onLogoUrlChange,
label = { Text("Logo-URL oder Pfad") }, label = { Text("Logo-URL oder Pfad") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
supportingText = { Text("Optional: Link zu einem Turnierlogo") } supportingText = { Text("Optional: Link zu einem Turnierlogo") }
@ -726,41 +699,216 @@ fun VeranstaltungKonfigV2(
OutlinedTextField( OutlinedTextField(
value = sponsorenText, value = sponsorenText,
onValueChange = { sponsorenText = it }, onValueChange = onSponsorenTextChange,
label = { Text("Sponsoren (mit Komma trennen)") }, label = { Text("Sponsoren (mit Komma trennen)") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
minLines = 3 minLines = 3
) )
Spacer(Modifier.height(16.dp)) Text("Vorschau Sponsoren:", style = MaterialTheme.typography.labelMedium, color = Color.Gray)
Text(
"Vorschau Sponsoren:",
style = MaterialTheme.typography.labelMedium,
color = Color.Gray
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
sponsorenText.split(",").filter { it.isNotBlank() }.forEach { sponsor -> sponsorenText.split(",").filter { it.isNotBlank() }.forEach { sponsor ->
SuggestionChip(onClick = {}, label = { Text(sponsor.trim()) }) SuggestionChip(onClick = {}, label = { Text(sponsor.trim()) })
} }
} }
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Prüfungen / Bewerbe", style = MaterialTheme.typography.titleMedium)
Text("Erfassen Sie die geplanten Bewerbe gemäß ÖTO.", style = MaterialTheme.typography.bodySmall)
if (bewerbe.isEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Box(Modifier.padding(24.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
"Noch keine Bewerbe hinzugefügt.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
} else {
bewerbe.forEachIndexed { index, b ->
ListItem(
headlineContent = { Text("Bewerb ${b.nummer}${b.abteilung?.let { " / $it" } ?: ""}: ${b.bezeichnung}") },
supportingContent = { Text("Klasse ${b.klasse} | ${b.disziplin}") },
trailingContent = {
IconButton(onClick = { bewerbe.removeAt(index) }) {
Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error)
}
},
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surface)
)
} }
} }
// Footer Navigation var newNr by remember { mutableStateOf("") }
Row( var newAbt by remember { mutableStateOf("") }
var newKlasse by remember { mutableStateOf("") }
var newBez by remember { mutableStateOf("") }
var newDis by remember { mutableStateOf(selectedDisziplinen.firstOrNull() ?: "Springen") }
Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = newNr,
onValueChange = { newNr = it },
label = { Text("Nr.") },
modifier = Modifier.width(60.dp),
singleLine = true
)
OutlinedTextField(
value = newAbt,
onValueChange = { newAbt = it },
label = { Text("Abt.") },
modifier = Modifier.width(60.dp),
singleLine = true
)
OutlinedTextField(
value = newBez,
onValueChange = { newBez = it },
label = { Text("Bezeichnung") },
modifier = Modifier.weight(1f),
singleLine = true
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = newKlasse,
onValueChange = { newKlasse = it },
label = { Text("Klasse") },
modifier = Modifier.weight(1f),
singleLine = true
)
OutlinedTextField(
value = newDis,
onValueChange = { newDis = it },
label = { Text("Disziplin") },
modifier = Modifier.weight(1f),
singleLine = true
)
Button(
onClick = {
if (newNr.isNotBlank() && newBez.isNotBlank()) {
bewerbe.add(BewerbData(newNr, newAbt.ifBlank { null }, newKlasse, newDis, newBez))
newNr = ""; newAbt = ""; newBez = ""
}
},
shape = RoundedCornerShape(8.dp)
) {
Icon(Icons.Default.Add, null)
Text("Add")
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VeranstaltungKonfig(
veranstalterId: Long = 0,
onBack: () -> Unit,
onSaved: (Long, Long) -> Unit, // eventId, veranstalterId
onVeranstalterCreated: (Long) -> Unit = {},
) {
val znsImporter: ZnsImportProvider = koinInject()
val znsState = znsImporter.state
DesktopThemeV2 {
var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) }
var selectedVereinId by remember { mutableStateOf(veranstalterId) }
var titel by remember { mutableStateOf("") }
var untertitel by remember { mutableStateOf("") }
var von by remember { mutableStateOf("") }
var bis by remember { mutableStateOf("") }
var ort by remember { mutableStateOf("") }
var plz by remember { mutableStateOf("") }
var selectedDisziplinen by remember { mutableStateOf(setOf("Springen")) }
var logoUrl by remember { mutableStateOf("") }
var sponsorenText by remember { mutableStateOf("") }
val bewerbe = remember { mutableStateListOf<BewerbData>() }
val dateFormatter = remember { DateTimeFormatter.ISO_LOCAL_DATE }
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
IconButton(onClick = {
if (currentStep > 1) {
if (veranstalterId != 0L) onBack() else currentStep--
} else onBack()
}) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück") }
Column {
Text("Neue Veranstaltung anlegen", style = MaterialTheme.typography.headlineSmall)
Text(
when (currentStep) {
1 -> "Schritt 1: Veranstalter auswählen"
2 -> "Schritt 2: Basisdaten"
3 -> "Schritt 3: Details & Bewerbe"
else -> ""
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
LinearProgressIndicator(
progress = { currentStep / 3f },
modifier = Modifier.fillMaxWidth().height(4.dp),
color = MaterialTheme.colorScheme.primary
)
Box(Modifier.weight(1f).fillMaxWidth()) {
when (currentStep) {
1 -> Step1Veranstalter(znsState, znsImporter, selectedVereinId, { selectedVereinId = it }, {
selectedVereinId = it; currentStep = 2
})
2 -> Step2Basisdaten(
titel,
{ titel = it },
untertitel,
{ untertitel = it },
von,
{ von = it },
bis,
{ bis = it },
ort,
{ ort = it },
plz,
{ plz = it },
selectedDisziplinen,
{ selectedDisziplinen = it },
dateFormatter
)
3 -> Step3Details(
logoUrl,
{ logoUrl = it },
sponsorenText,
{ sponsorenText = it },
bewerbe,
selectedDisziplinen
)
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (currentStep > 1) { if (currentStep > 1) OutlinedButton(onClick = { currentStep-- }) { Text("Zurück") }
OutlinedButton(onClick = { currentStep-- }) { else Spacer(Modifier.width(1.dp))
Text("Zurück")
}
} else {
Spacer(Modifier.width(1.dp))
}
var showConfirm by remember { mutableStateOf(false) } var showConfirm by remember { mutableStateOf(false) }
if (showConfirm) { if (showConfirm) {
@ -769,55 +917,30 @@ fun VeranstaltungKonfigV2(
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
val id = System.currentTimeMillis() val id = System.currentTimeMillis()
val v = VeranstaltungV2( val v = Veranstaltung(
id = id, id = id,
veranstalterId = selectedVereinId, veranstalterId = selectedVereinId,
titel = titel.trim(), titel = titel.trim(),
datumVon = von.trim(), datumVon = von.trim(),
datumBis = bis.trim().ifBlank { null }, datumBis = bis.trim().ifBlank { null },
untertitel = untertitel.trim(), untertitel = untertitel.trim(),
ort = ort.trim().ifBlank { StoreV2.vereine.find { it.id == selectedVereinId }?.ort ?: "" }, ort = ort.trim().ifBlank { Store.vereine.find { it.id == selectedVereinId }?.ort ?: "" },
logoUrl = logoUrl.trim().ifBlank { null } logoUrl = logoUrl.trim().ifBlank { null }
) )
sponsorenText.split(",").filter { it.isNotBlank() }.forEach { v.sponsoren.add(it.trim()) } sponsorenText.split(",").filter { it.isNotBlank() }.forEach { v.sponsoren.add(it.trim()) }
StoreV2.addEventFirst(selectedVereinId, v) Store.addEventFirst(selectedVereinId, v)
showConfirm = false showConfirm = false
onSaved(id, selectedVereinId) onSaved(id, selectedVereinId)
}) { Text("Anlegen") } }) { Text("Anlegen") }
}, },
dismissButton = { TextButton(onClick = { showConfirm = false }) { Text("Abbrechen") } }, dismissButton = { TextButton(onClick = { showConfirm = false }) { Text("Abbrechen") } },
title = { Text("Veranstaltung final anlegen?") }, title = { Text("Veranstaltung final anlegen?") },
text = { text = { Text("Bitte die Daten prüfen. Titel: ${titel.trim()}, Veranstalter: ${Store.vereine.find { it.id == selectedVereinId }?.name ?: ""}") }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Bitte die Daten prüfen. Für diese Veranstaltung wird eine eigene Datenbank initialisiert.")
HorizontalDivider()
val titelText = titel.trim()
val untertitelText = untertitel.trim().ifBlank { "-" }
val vonText = von.trim()
val bisText = bis.trim()
val zeitraumText = if (bisText.isNotEmpty()) "$vonText $bisText" else vonText
val vName = StoreV2.vereine.find { it.id == selectedVereinId }?.name ?: "#$selectedVereinId"
val spons = sponsorenText.split(',').map { it.trim() }.filter { it.isNotEmpty() }
Text("Titel: $titelText")
Text("Untertitel: $untertitelText")
Text("Zeitraum: $zeitraumText")
Text("Veranstalter: $vName")
if (logoUrl.isNotBlank()) Text("Logo: ${logoUrl.trim()}")
if (spons.isNotEmpty()) Text("Sponsoren: ${spons.joinToString(", ")}")
}
}
) )
} }
Button( Button(
onClick = { onClick = { if (currentStep < 3) currentStep++ else showConfirm = true },
if (currentStep < 3) {
currentStep++
} else {
showConfirm = true
}
},
enabled = when (currentStep) { enabled = when (currentStep) {
1 -> selectedVereinId != 0L 1 -> selectedVereinId != 0L
2 -> { 2 -> {
@ -831,24 +954,21 @@ fun VeranstaltungKonfigV2(
} catch (_: Exception) { } catch (_: Exception) {
null null
} }
val today2 = LocalDate.now() val rangeInvalid =
val startInPast = dVon != null && dVon.isBefore(today2) (dVon != null && dBis != null && dBis.isBefore(dVon)) || (dVon != null && dVon.isBefore(LocalDate.now()))
val rangeInvalid = (dVon != null && dBis != null && dBis.isBefore(dVon)) || startInPast
titel.isNotBlank() && von.isNotBlank() && !rangeInvalid titel.isNotBlank() && von.isNotBlank() && !rangeInvalid
} }
3 -> true 3 -> bewerbe.isNotEmpty()
else -> false else -> false
} }
) { ) { Text(if (currentStep == 3) "Veranstaltung final anlegen" else "Weiter") }
Text(if (currentStep == 3) "Veranstaltung final anlegen" else "Weiter")
}
} }
} }
} }
} }
data class TurnierV2( data class Turnier(
val id: Long, val id: Long,
val veranstaltungId: Long, val veranstaltungId: Long,
val turnierNr: Int, val turnierNr: Int,
@ -864,15 +984,17 @@ data class TurnierV2(
var sponsoren: SnapshotStateList<String> = mutableStateListOf(), var sponsoren: SnapshotStateList<String> = mutableStateListOf(),
) )
object TurnierStoreV2 { object TurnierStore {
private val map = mutableMapOf<Long, MutableList<TurnierV2>>() private val map = mutableMapOf<Long, MutableList<Turnier>>()
fun list(veranstaltungId: Long): MutableList<TurnierV2> = map.getOrPut(veranstaltungId) { mutableListOf() } fun list(veranstaltungId: Long): MutableList<Turnier> = map.getOrPut(veranstaltungId) { mutableListOf() }
fun add(veranstaltungId: Long, t: TurnierV2) { list(veranstaltungId).add(0, t) } fun add(veranstaltungId: Long, t: Turnier) {
list(veranstaltungId).add(0, t)
}
fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } } fun remove(veranstaltungId: Long, tId: Long) { list(veranstaltungId).removeAll { it.id == tId } }
// Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab) // Hilfsmethode für Reflection-Zugriff aus anderen Modulen (StammdatenTab)
@JvmStatic @JvmStatic
fun allTurniere(): List<TurnierV2> = map.values.flatten() fun allTurniere(): List<Turnier> = map.values.flatten()
} }
@Composable @Composable
@ -885,8 +1007,8 @@ fun VeranstaltungProfilScreen(
onNavigateToVeranstalterProfil: (Long) -> Unit, onNavigateToVeranstalterProfil: (Long) -> Unit,
) { ) {
DesktopThemeV2 { DesktopThemeV2 {
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
val turniere = remember(veranstaltungId) { TurnierStoreV2.list(veranstaltungId) } val turniere = remember(veranstaltungId) { TurnierStore.list(veranstaltungId) }
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) { Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
// Header // Header
@ -987,7 +1109,7 @@ fun VeranstaltungProfilScreen(
TurnierCard( TurnierCard(
turnier = t, turnier = t,
onOpen = { onTurnierOpen(t.id) }, onOpen = { onTurnierOpen(t.id) },
onDelete = { TurnierStoreV2.remove(veranstaltungId, t.id) } onDelete = { TurnierStore.remove(veranstaltungId, t.id) }
) )
} }
} }
@ -1028,7 +1150,7 @@ private fun KpiCard(
@Composable @Composable
private fun TurnierCard( private fun TurnierCard(
turnier: TurnierV2, turnier: Turnier,
onOpen: () -> Unit, onOpen: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
) { ) {
@ -1100,14 +1222,14 @@ private fun TurnierCard(
} }
@Composable @Composable
fun TurnierWizardV2( fun TurnierWizard(
veranstalterId: Long, veranstalterId: Long,
veranstaltungId: Long, veranstaltungId: Long,
onBack: () -> Unit, onBack: () -> Unit,
onSaved: (Long) -> Unit, onSaved: (Long) -> Unit,
) { ) {
DesktopThemeV2 { DesktopThemeV2 {
val veranstaltung = StoreV2.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId } val veranstaltung = Store.eventsFor(veranstalterId).firstOrNull { it.id == veranstaltungId }
var currentStep by remember { mutableStateOf(1) } var currentStep by remember { mutableStateOf(1) }
var showZnsDialog by remember { mutableStateOf(false) } var showZnsDialog by remember { mutableStateOf(false) }
@ -1222,7 +1344,7 @@ fun TurnierWizardV2(
currentStep++ currentStep++
} else { } else {
val id = System.currentTimeMillis() val id = System.currentTimeMillis()
val newTurnier = TurnierV2( val newTurnier = Turnier(
id = id, id = id,
veranstaltungId = veranstaltungId, veranstaltungId = veranstaltungId,
turnierNr = nr.toInt(), turnierNr = nr.toInt(),
@ -1238,7 +1360,7 @@ fun TurnierWizardV2(
newTurnier.kategorie.addAll(kat) newTurnier.kategorie.addAll(kat)
newTurnier.sponsoren.addAll(sponsoren) newTurnier.sponsoren.addAll(sponsoren)
TurnierStoreV2.add(veranstaltungId, newTurnier) TurnierStore.add(veranstaltungId, newTurnier)
onSaved(id) onSaved(id)
} }
}, },
@ -1381,7 +1503,7 @@ private fun Step2Sparten(
kat: SnapshotStateList<String>, kat: SnapshotStateList<String>,
von: String, onVonChange: (String) -> Unit, von: String, onVonChange: (String) -> Unit,
bis: String, onBisChange: (String) -> Unit, bis: String, onBisChange: (String) -> Unit,
veranstaltung: VeranstaltungV2? veranstaltung: Veranstaltung?
) { ) {
var showDatePickerVon by remember { mutableStateOf(false) } var showDatePickerVon by remember { mutableStateOf(false) }
var showDatePickerBis by remember { mutableStateOf(false) } var showDatePickerBis by remember { mutableStateOf(false) }