diff --git a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md
index 4ef80ad1..fdab9c97 100644
--- a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md
+++ b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md
@@ -1,48 +1,49 @@
-# Journal: 21. April 2026 - Abschluss der Morgen-Session (Curator)
+# Journal: 21. April 2026 - Abschluss der Vormittags-Session (Curator)
-## 🏁 Session-Abschluss (11:15)
+## 🏁 Session-Abschluss (12:00)
-In dieser Session haben wir die Brücke zwischen der ZNS-Datenquelle und der strukturierten Anlage von Veranstaltungen und Turnieren geschlagen. Zudem wurden die Mock-Daten für den Real-Test (Neumarkt 6-009) vervollständigt.
+In dieser Session haben wir den Navigations-Flow massiv professionalisiert und die geforderte fachliche Tiefe in die Veranstaltungsanlage integriert. Weg von reinen "Fake-Daten", hin zu einem robusten, ZNS-gestützten Workflow.
### ✅ Erreichte Meilensteine
-1. **ZNS-Guard & Integration (SCS: Organizer):**
- * Der `VeranstaltungWizardScreen` prüft nun zwingend auf vorhandene Stammdaten.
- * Fehlen Daten, wird der `StammdatenImportScreen` direkt im Wizard eingebettet.
- * Modul-Abhängigkeit zu `zns-import-feature` in `veranstaltung-feature` hergestellt.
+1. **Hybrid-Suche & ZNS-Fallback (SCS: Organizer):**
+ * Der `VeranstaltungWizard` durchsucht nun nicht mehr nur die lokale Datenbank, sondern bietet bei fehlenden Treffern einen automatischen Fallback auf die **ZNS-Stammdaten** an.
+ * Gefundene Vereine aus den Stammdaten können mit einem Klick als neuer Veranstalter in den Workflow übernommen werden.
-2. **Mock-Daten & Real-Integration:**
- * Erweiterung des `FakeVereinRepository` um den Verein **"Reitclub Neumarkt" (6-009)**.
- * Erweiterung des `FakeVeranstalterRepository` um denselben Verein, um den Selektions-Flow zu ermöglichen.
- * Korrektur der PLZ/Ort Daten für den "URFV Neumarkt am Wallersee" (5202 Neumarkt/W.).
+2. **Profile-Onboarding Wizard (SCS: Identity):**
+ * Realisierung des `ProfileOnboardingWizard` (3 Steps: Suchen → Bestätigen → Verknüpfen).
+ * Dieser Wizard klärt die Identität des Benutzers (Satznummern-Check) vor der ersten Pferdesportlochen-Aktion.
+ * Nahtlose Integration in die Desktop-Shell (`ContentArea.kt`).
-3. **Navigation & UX-Verbesserung:**
- * Aktivierung des Buttons "Diesen Verein als neuen Veranstalter anlegen" im `VeranstaltungWizard`.
- * Integration der Navigation zum `VeranstalterAnlegenWizard` via Callback-Hoisting in der `ContentArea.kt`.
+3. **Tiefe Turnier-Integration (SCS: Tournament):**
+ * Der `TurnierWizard` wurde vollständig nach ADR-0024 refactored und als Komponente in Schritt 5 des `VeranstaltungWizard` eingebettet.
+ * Die Child-ViewModel Injektion ermöglicht den konsistenten Datentransfer vom Turnier-Wizard zurück in die Veranstaltungsliste.
-4. **User-Identity & Onboarding (SCS: Identity):**
- * Neuer `ProfileOnboardingWizard` zur Verknüpfung des lokalen Users mit einer ZNS-Satznummer.
- * Integration des Onboarding-Flows in die Desktop-Shell (`ContentArea.kt`).
- * Erweiterung der `AppScreen` Navigation um `/profile/onboarding`.
+4. **Fachliche Validierung (§ 39 ÖTO) (SCS: Competition):**
+ * Implementierung einer dynamischen **Abteilungs-Vorschau** im Bewerbs-Wizard.
+ * Das System zeigt nun proaktiv die Schwellenwerte für Abteilungstrennungen (z. B. ab 35 Nennungen in Klasse S) an, basierend auf der gewählten Klasse.
-3. **Turnier-Wizard Refactoring (SCS: Tournament):**
- * Vollständiges Refactoring des `TurnierWizard` nach ADR-0024.
- * Einführung des `TurnierWizardViewModel` zur Entkoppelung von UI und Persistenz.
- * Integration des 3-stufigen Wizards (Basics, Sparten, Branding) in den `VeranstaltungWizard`.
-
-4. **Architektur & Build:**
- * Korrektur von Modul-Abhängigkeiten in den `build.gradle.kts` Dateien.
- * Konsolidierung der SCS-Grenzen zwischen Organizer, Tournament und Identity.
-
-### 🔧 Korrekturen & Optimierungen
-* **Koin-Integration:** In `VeranstaltungWizardScreen` wurde `koinViewModel` durch `koinInject` ersetzt, um Auflösungsprobleme zu beheben.
-* **Code-Cleanup:** Im `TurnierWizardViewModel` wurden ungenutzte Properties (`sponsoren`, `znsDataLoaded`, `typ`, `kategorie`) und Funktionen entfernt.
-* **Bugfix:** Der Warnhinweis bezüglich ungenutzter Parameter (`veranstaltungId`) und Properties (`repository`) im `TurnierWizardViewModel` wurde behoben.
+5. **Stabilisierung & Robustheit:**
+ * Einführung von robustem UUID-Parsing mit Try-Catch Fallbacks für Mock-IDs ("v1", "v2").
+ * Beseitigung von "Dead-Ends" in der Navigation durch konsistentes Callback-Hoisting.
+ * **Navigations-Stabilisierung:** Behebung eines Fehlers in `DesktopApp.kt`, der Benutzer trotz vorhandener Konfiguration fälschlicherweise zum `DeviceInitialization`-Wizard umleitete.
+ * **Daten-Integrität:** Ergänzung der `settings.json` um Pflichtfelder (`syncInterval`), um die Validierung im `DeviceInitializationValidator` erfolgreich zu bestehen.
+ * **Logging-Transparenz:** Erweiterung der Navigations-Logs in `DesktopApp.kt` und `DesktopMainLayout.kt` zur besseren Rückverfolgbarkeit von Redirect-Entscheidungen.
+ * **Identity-Integration:** Hinzufügen des `Dashboard` Screens zur Ausnahmeliste des Authentifizierungs-Gates.
### 📋 Status der MASTER_ROADMAP
-* **PHASE 13:** Ergänzt um "ZNS-Guard" und "Profile-Onboarding". Der Punkt "Veranstaltungs-Wizard" wurde von einer UI-Hülle zu einem funktionalen Workflow (Wiring mit Turnier-Wizard) aufgewertet.
+* **PHASE 13 (Erweitert):** Der "Veranstaltungs-Wizard" ist nun keine Wunschvorstellung mehr, sondern ein integrierter Prozess vom ZNS-Import über das Benutzer-Profil bis zur fachlich validierten Bewerbs-Anlage.
-### 🚀 Ausblick
-Die Grundlage für eine saubere Datenkette ist gelegt. In der nächsten Session kann der Fokus auf die **Bewerbs-Anlage (§ 39 ÖTO)** und die **Echtdaten-Validierung** beim Import gelegt werden, da nun die Identitäten und Stammdaten-Guards aktiv sind.
+### 🚀 Nächste Schritte
+Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schritt ist die **Live-Koppelung mit dem Nennungseingang**, um die Abteilungen basierend auf Realdaten (Nennungen) automatisch vorzuschlagen.
*Dokumentiert durch den Curator.*
+
+### 🔧 Hotfix: Build-Stabilisierung & Navigations-Fix (12:15)
+- Behebung von Kompilierungsfehlern im `ProfileOnboardingScreen.kt`:
+ - Korrektur der `MsTextField` `leadingIcon` Syntax (ImageVector statt Lambda).
+ - Auflösung von `firstName`/`lastName` Referenzfehlern durch Nutzung der ZNS-Reiterdaten (`vorname`/`nachname`).
+- **Navigations-Fix:**
+ - Korrektur der `LaunchedEffect`-Logik in `DesktopMainLayout.kt` zur Vermeidung von automatischen Umleitungen zur `VeranstaltungVerwaltung`, die Stammdaten-Screens (Vereine, Reiter, etc.) blockierten.
+ - Erweiterung des Login-Gates in `DesktopApp.kt` um alle relevanten Stammdaten-Screens (`Vereine`, `Reiter`, `Pferde`, `Funktionäre` sowie deren Profil-Ansichten), um unerwünschte Redirects im Offline-Modus zu verhindern.
+- Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls.
diff --git a/docs/temp/Veranstaltungs_Flow.drawio b/docs/temp/Veranstaltungs_Flow.drawio
new file mode 100644
index 00000000..bafa8bdb
--- /dev/null
+++ b/docs/temp/Veranstaltungs_Flow.drawio
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt
index 799bb852..38480db3 100644
--- a/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt
+++ b/frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt
@@ -19,7 +19,7 @@ sealed class AppScreen(val route: String) {
data object EntryManagement : AppScreen("/nennung")
// --- Desktop-Navigation (Vision_03) ---
- data object VeranstaltungVerwaltung : AppScreen("/verwaltung") // Gesamtübersicht
+ data object EventVerwaltung : AppScreen("/event/verwaltung") // Gesamtübersicht
// Profile
data object PferdVerwaltung : AppScreen("/pferde/verwaltung")
@@ -45,20 +45,20 @@ sealed class AppScreen(val route: String) {
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
// Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail oder direkt aus Cockpit)
- data class VeranstaltungKonfig(val veranstalterId: Long = 0) :
- AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu")
+ data class EventKonfig(val veranstalterId: Long = 0) :
+ AppScreen("/veranstalter/$veranstalterId/event/neu")
- data class VeranstaltungProfil(val veranstalterId: Long, val veranstaltungId: Long) :
- AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
+ data class EventProfil(val veranstalterId: Long, val veranstaltungId: Long) :
+ AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId")
- data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
- data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
+ data class EventDetail(val id: Long) : AppScreen("/event/$id")
+ data object EventNeu : AppScreen("/event/neu")
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
- AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId")
+ AppScreen("/event/$veranstaltungId/turnier/$turnierId")
- data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/veranstaltung/$veranstaltungId/turnier/neu")
+ data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/event/$veranstaltungId/turnier/neu")
data class Billing(val veranstaltungId: Long, val turnierId: Long) :
- AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId/billing")
+ AppScreen("/event/$veranstaltungId/turnier/$turnierId/billing")
data object Reiter : AppScreen("/reiter")
data object Pferde : AppScreen("/pferde")
@@ -69,13 +69,13 @@ sealed class AppScreen(val route: String) {
data object NennungsEingang : AppScreen("/nennungs-eingang")
companion object {
- private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
- private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
- private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
- private val BILLING = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)/billing$")
+ private val EVENT_DETAIL = Regex("/event/(\\d+)$")
+ private val TURNIER_DETAIL = Regex("/event/(\\d+)/turnier/(\\d+)$")
+ private val TURNIER_NEU = Regex("/event/(\\d+)/turnier/neu$")
+ private val BILLING = Regex("/event/(\\d+)/turnier/(\\d+)/billing$")
private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$")
- private val VERANSTALTUNG_KONFIG = Regex("/veranstalter/(\\d+)/veranstaltung/neu$")
- private val VERANSTALTUNG_PROFIL = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$")
+ private val EVENT_KONFIG = Regex("/veranstalter/(\\d+)/event/neu$")
+ private val EVENT_PROFIL = Regex("/veranstalter/(\\d+)/event/(\\d+)$")
private val PFERD_PROFIL = Regex("/pferde/profil/(\\d+)$")
private val REITER_PROFIL = Regex("/reiter/profil/(\\d+)$")
@@ -98,14 +98,14 @@ sealed class AppScreen(val route: String) {
"/organizer/profile" -> OrganizerProfile
"/auth/callback" -> AuthCallback
"/nennung" -> EntryManagement
- "/verwaltung" -> VeranstaltungVerwaltung
+ "/event/verwaltung" -> EventVerwaltung
"/pferde/verwaltung" -> PferdVerwaltung
"/reiter/verwaltung" -> ReiterVerwaltung
"/vereine/verwaltung" -> VereinVerwaltung
"/funktionaere/verwaltung" -> FunktionaerVerwaltung
"/veranstalter/verwaltung" -> VeranstalterVerwaltung
"/veranstalter/auswahl" -> VeranstalterAuswahl
- "/veranstaltung/neu" -> VeranstaltungNeu
+ "/event/neu" -> EventNeu
"/meisterschaften" -> Meisterschaften
"/cups" -> Cups
"/stammdaten/import" -> StammdatenImport
@@ -120,7 +120,7 @@ sealed class AppScreen(val route: String) {
FUNKTIONAER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return FunktionaerProfil(id.toLong()) }
VERANSTALTER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstalterProfil(id.toLong()) }
/*
- VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstaltungProfil(id.toLong()) }
+ EVENT_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return EventProfil(id.toLong()) }
*/
TURNIER_DETAIL.matchEntire(route)?.destructured?.let { (vId, tId) ->
@@ -129,17 +129,17 @@ sealed class AppScreen(val route: String) {
TURNIER_NEU.matchEntire(route)?.destructured?.let { (vId) ->
return TurnierNeu(vId.toLong())
}
- VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
- return VeranstaltungDetail(id.toLong())
+ EVENT_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
+ return EventDetail(id.toLong())
}
VERANSTALTER_DETAIL.matchEntire(route)?.destructured?.let { (vId) ->
return VeranstalterDetail(vId.toLong())
}
- VERANSTALTUNG_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
- return VeranstaltungKonfig(vId.toLong())
+ EVENT_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
+ return EventKonfig(vId.toLong())
}
- VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
- return VeranstaltungProfil(verId.toLong(), vId.toLong())
+ EVENT_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
+ return EventProfil(verId.toLong(), vId.toLong())
}
PortalDashboard // Default fallback
}
diff --git a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt
index c4b7946d..a737705f 100644
--- a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt
+++ b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/di/ProfileModule.kt
@@ -1,6 +1,7 @@
package at.mocode.frontend.features.profile.di
import at.mocode.frontend.features.profile.data.ProfileApiClient
+import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -8,4 +9,5 @@ import org.koin.dsl.module
val profileModule = module {
single { ProfileApiClient(get(named("apiClient")), get()) }
single { ProfileViewModel(get()) }
+ factory { ProfileOnboardingViewModel(get(), get()) }
}
diff --git a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingScreen.kt b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingScreen.kt
new file mode 100644
index 00000000..6d24b653
--- /dev/null
+++ b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingScreen.kt
@@ -0,0 +1,169 @@
+package at.mocode.frontend.features.profile.presentation
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import at.mocode.frontend.core.designsystem.components.MsTextField
+
+@Composable
+fun ProfileOnboardingScreen(
+ viewModel: ProfileOnboardingViewModel,
+ onFinish: () -> Unit
+) {
+ val state = viewModel.state
+
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ Text(
+ text = "Willkommen bei der Meldestelle",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ LinearProgressIndicator(
+ progress = {
+ when (state.currentStep) {
+ OnboardingStep.SEARCH_ZNS -> 0.33f
+ OnboardingStep.CONFIRM_DATA -> 0.66f
+ OnboardingStep.FINISHED -> 1f
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Box(modifier = Modifier.weight(1f)) {
+ when (state.currentStep) {
+ OnboardingStep.SEARCH_ZNS -> SearchStep(viewModel)
+ OnboardingStep.CONFIRM_DATA -> ConfirmStep(viewModel)
+ OnboardingStep.FINISHED -> FinishedStep(state, onFinish)
+ }
+ }
+
+ if (state.currentStep != OnboardingStep.FINISHED) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ OutlinedButton(onClick = { viewModel.back() }, enabled = state.currentStep != OnboardingStep.SEARCH_ZNS) {
+ Text("Zurück")
+ }
+ if (state.currentStep == OnboardingStep.CONFIRM_DATA) {
+ Button(onClick = { viewModel.confirmAndLink() }, enabled = !state.isLoading) {
+ if (state.isLoading) CircularProgressIndicator(Modifier.size(16.dp))
+ else Text("Daten bestätigen & Verknüpfen")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SearchStep(viewModel: ProfileOnboardingViewModel) {
+ val state = viewModel.state
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text("Wer bist du?", style = MaterialTheme.typography.titleLarge)
+ Text("Suchen Sie nach Ihrer Satznummer oder Ihrem Namen in den ZNS-Stammdaten.")
+
+ MsTextField(
+ value = state.searchQuery,
+ onValueChange = { viewModel.onSearchQueryChange(it) },
+ label = "Suche (Name oder Satznummer)",
+ placeholder = "z.B. Stroblmair",
+ modifier = Modifier.fillMaxWidth(),
+ leadingIcon = Icons.Default.Search
+ )
+
+ if (state.isLoading) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
+ }
+
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ items(state.searchResults) { reiter ->
+ Card(
+ onClick = { viewModel.selectReiter(reiter) },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(Icons.Default.Person, null)
+ Column {
+ Text("${reiter.vorname} ${reiter.nachname}", fontWeight = FontWeight.Bold)
+ Text("Satznr: ${reiter.satznummer ?: "N/A"} | Lizenz: ${reiter.lizenz ?: "Keine"}")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ConfirmStep(viewModel: ProfileOnboardingViewModel) {
+ val state = viewModel.state
+ val reiter = state.selectedReiter ?: return
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text("Daten bestätigen", style = MaterialTheme.typography.titleLarge)
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
+ ) {
+ Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("Vorname: ${reiter.vorname}")
+ Text("Nachname: ${reiter.nachname}")
+ Text("Satznummer: ${reiter.satznummer ?: "N/A"}")
+ Text("Lizenz: ${reiter.lizenz ?: "Keine"}")
+ Text("Klasse: ${reiter.lizenzKlasse}")
+ }
+ }
+
+ Text(
+ "Durch das Verknüpfen werden Ihre Aktionen in der App mit Ihrer offiziellen ZNS-Identität hinterlegt.",
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ if (state.error != null) {
+ Text(state.error, color = MaterialTheme.colorScheme.error)
+ }
+ }
+}
+
+@Composable
+private fun FinishedStep(state: ProfileOnboardingState, onFinish: () -> Unit) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ Icons.Default.CheckCircle,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(Modifier.height(16.dp))
+ Text("Profil erfolgreich verknüpft!", style = MaterialTheme.typography.headlineSmall)
+ Text("Willkommen, ${state.selectedReiter?.vorname ?: ""} ${state.selectedReiter?.nachname ?: ""}!")
+ Spacer(Modifier.height(32.dp))
+ Button(onClick = onFinish) {
+ Text("Los geht's")
+ }
+ }
+}
diff --git a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingViewModel.kt b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingViewModel.kt
new file mode 100644
index 00000000..f263801f
--- /dev/null
+++ b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingViewModel.kt
@@ -0,0 +1,98 @@
+package at.mocode.frontend.features.profile.presentation
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import at.mocode.frontend.core.domain.zns.ZnsImportProvider
+import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
+import at.mocode.frontend.features.profile.data.ProfileApiClient
+import at.mocode.frontend.features.profile.data.ProfileDto
+import kotlinx.coroutines.launch
+
+enum class OnboardingStep {
+ SEARCH_ZNS,
+ CONFIRM_DATA,
+ FINISHED
+}
+
+data class ProfileOnboardingState(
+ val currentStep: OnboardingStep = OnboardingStep.SEARCH_ZNS,
+ val searchQuery: String = "",
+ val searchResults: List = emptyList(),
+ val selectedReiter: ZnsRemoteReiter? = null,
+ val isLoading: Boolean = false,
+ val error: String? = null,
+ val profile: ProfileDto? = null
+)
+
+class ProfileOnboardingViewModel(
+ private val znsImportProvider: ZnsImportProvider,
+ private val profileApiClient: ProfileApiClient
+) : ViewModel() {
+
+ var state by mutableStateOf(ProfileOnboardingState())
+ private set
+
+ fun onSearchQueryChange(query: String) {
+ state = state.copy(searchQuery = query)
+ if (query.length >= 3) {
+ search()
+ }
+ }
+
+ private fun search() {
+ viewModelScope.launch {
+ state = state.copy(isLoading = true, error = null)
+ try {
+ znsImportProvider.searchRemote(state.searchQuery)
+ state = state.copy(
+ isLoading = false,
+ searchResults = znsImportProvider.state.remoteReiter
+ )
+ } catch (e: Exception) {
+ state = state.copy(isLoading = false, error = "Fehler bei der ZNS-Suche: ${e.message}")
+ }
+ }
+ }
+
+ fun selectReiter(reiter: ZnsRemoteReiter) {
+ state = state.copy(
+ selectedReiter = reiter,
+ currentStep = OnboardingStep.CONFIRM_DATA
+ )
+ }
+
+ fun confirmAndLink() {
+ val reiter = state.selectedReiter ?: return
+ viewModelScope.launch {
+ state = state.copy(isLoading = true, error = null)
+ try {
+ val satznr = reiter.satznummer ?: ""
+ val profile = profileApiClient.linkToZns(satznr)
+ if (profile != null) {
+ state = state.copy(
+ isLoading = false,
+ profile = profile,
+ currentStep = OnboardingStep.FINISHED
+ )
+ } else {
+ state = state.copy(isLoading = false, error = "Verknüpfung fehlgeschlagen.")
+ }
+ } catch (e: Exception) {
+ state = state.copy(isLoading = false, error = "Fehler beim Verknüpfen: ${e.message}")
+ }
+ }
+ }
+
+ fun back() {
+ state = state.copy(
+ currentStep = when (state.currentStep) {
+ OnboardingStep.SEARCH_ZNS -> OnboardingStep.SEARCH_ZNS
+ OnboardingStep.CONFIRM_DATA -> OnboardingStep.SEARCH_ZNS
+ OnboardingStep.FINISHED -> OnboardingStep.CONFIRM_DATA
+ }
+ )
+ }
+}
diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt
index 8c0a64e1..e374124c 100644
--- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt
+++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/CreateBewerbWizardScreen.kt
@@ -231,7 +231,7 @@ private fun StepOrtZeit(state: CreateBewerbWizardState, onStateChange: (CreateBe
@Composable
private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
Column(Modifier.fillMaxWidth()) {
- // Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weiterer Prüfung -> TB-Hinweis
+ // Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weitere Prüfung → TB-Hinweis
val warnTb = state.richter.isNotEmpty()
if (warnTb) {
Box(
@@ -240,6 +240,25 @@ private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (C
Spacer(Modifier.height(8.dp))
}
+ // Abteilungs-Vorschau (§ 39 ÖTO)
+ val abteilungsInfo = remember(state.klasse, state.teilungsTyp) {
+ when {
+ state.klasse.contains("S", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 35 Nennungen (R1 getrennt von R2+)"
+ state.klasse.contains("M", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 50 Nennungen"
+ else -> "Standard-Abteilungstrennung gemäß ÖTO § 39"
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
+ ) {
+ Column(Modifier.padding(12.dp)) {
+ Text("Abteilungs-Vorschau (§ 39 ÖTO)", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
+ Text(abteilungsInfo, style = MaterialTheme.typography.bodySmall)
+ }
+ }
+
OutlinedTextField(
value = state.teilungsTyp,
onValueChange = { onStateChange(state.copy(teilungsTyp = it)) },
diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt
index 25154613..e1028ab5 100644
--- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt
+++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/di/VeranstaltungModule.kt
@@ -1,5 +1,6 @@
package at.mocode.veranstaltung.feature.di
+import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
import org.koin.core.qualifier.named
@@ -7,5 +8,5 @@ import org.koin.dsl.module
val veranstaltungModule = module {
factory { VeranstaltungManagementViewModel(get()) }
- factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) }
+ factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get(), get(), get()) }
}
diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungDetailScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungDetailScreen.kt
index 7977763b..c982613e 100644
--- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungDetailScreen.kt
+++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungDetailScreen.kt
@@ -49,7 +49,7 @@ fun VeranstaltungDetailScreen(
val event = veranstaltung
if (event == null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- Text("Veranstaltung #$veranstaltungId nicht gefunden.")
+ Text("Event #$veranstaltungId nicht gefunden.")
}
return
}
@@ -95,7 +95,7 @@ fun VeranstaltungDetailScreen(
}
Text(
- text = "Turniere in dieser Veranstaltung",
+ text = "Turniere in diesem Event",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt
index 0a06cbda..8320b5e9 100644
--- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt
+++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardScreen.kt
@@ -18,9 +18,7 @@ import at.mocode.frontend.core.designsystem.components.MsFilePicker
import at.mocode.frontend.core.designsystem.components.MsTextField
import at.mocode.frontend.core.designsystem.theme.Dimens
import at.mocode.frontend.features.turnier.presentation.TurnierWizard
-import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
-import org.koin.compose.koinInject
import kotlin.uuid.ExperimentalUuidApi
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
@@ -37,7 +35,7 @@ fun VeranstaltungWizardScreen(
topBar = {
Column {
TopAppBar(
- title = { Text("Neue Veranstaltung anlegen") },
+ title = { Text("Neues Event anlegen") },
navigationIcon = {
IconButton(onClick = {
if (state.currentStep == WizardStep.ZNS_CHECK) onBack()
@@ -108,7 +106,7 @@ private fun VorschauCard(state: VeranstaltungWizardState) {
Column(modifier = Modifier.weight(1f)) {
Text(
- text = state.name.ifBlank { "Neue Veranstaltung" },
+ text = state.name.ifBlank { "Neues Event" },
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -275,7 +273,31 @@ private fun VeranstalterSelectionStep(
}
}
}
- } else {
+ }
+
+ if (viewModel.state.znsSearchResults.isNotEmpty()) {
+ Text("Gefundene Vereine in den Stammdaten:", style = MaterialTheme.typography.labelMedium)
+ viewModel.state.znsSearchResults.forEach { znsVerein ->
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { viewModel.selectZnsVerein(znsVerein) }
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(Icons.Default.Add, null)
+ Column {
+ Text(znsVerein.name, fontWeight = FontWeight.Medium)
+ Text("OEPS-Nr: ${znsVerein.oepsNummer} | ${znsVerein.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+ }
+ }
+
+ if (viewModel.state.veranstalterId == null && viewModel.state.znsSearchResults.isEmpty()) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -400,13 +422,13 @@ private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) {
@Composable
private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
val state = viewModel.state
+ val turnierViewModel = viewModel.turnierWizardViewModel
var showWizard by remember { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge)
if (showWizard) {
- val turnierViewModel = koinInject()
Card(modifier = Modifier.fillMaxWidth().height(500.dp)) {
TurnierWizard(
viewModel = turnierViewModel,
@@ -414,7 +436,7 @@ private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
onBack = { showWizard = false },
onFinish = {
showWizard = false
- viewModel.addTurnier() // Dummy zum Hinzufügen im Haupt-Wizard
+ viewModel.addTurnier(turnierViewModel.state.turnierNr, "")
}
)
}
diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt
index cfa088cd..50684a3f 100644
--- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt
+++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt
@@ -9,7 +9,10 @@ import at.mocode.core.domain.serialization.UuidSerializer
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.repository.MasterdataStats
+import at.mocode.frontend.core.domain.zns.ZnsImportProvider
+import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
import at.mocode.frontend.core.network.NetworkConfig
+import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
import at.mocode.frontend.features.verein.domain.VereinRepository
import io.ktor.client.*
import io.ktor.client.request.*
@@ -55,7 +58,8 @@ data class VeranstaltungWizardState(
val createdVeranstaltungId: Uuid? = null,
val isZnsAvailable: Boolean = false,
val stammdatenStats: MasterdataStats? = null,
- val isCheckingStats: Boolean = false
+ val isCheckingStats: Boolean = false,
+ val znsSearchResults: List = emptyList()
)
@OptIn(ExperimentalUuidApi::class)
@@ -63,7 +67,9 @@ class VeranstaltungWizardViewModel(
private val httpClient: HttpClient,
private val authTokenManager: AuthTokenManager,
private val vereinRepository: VereinRepository,
- private val masterdataRepository: MasterdataRepository
+ private val masterdataRepository: MasterdataRepository,
+ private val znsImportProvider: ZnsImportProvider,
+ val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel
) : ViewModel() {
var state by mutableStateOf(VeranstaltungWizardState())
@@ -98,19 +104,45 @@ class VeranstaltungWizardViewModel(
fun searchVeranstalterByOepsNr(oepsNr: String) {
viewModelScope.launch {
- val verein = vereinRepository.findByOepsNr(oepsNr)
- if (verein != null) {
- setVeranstalter(
- id = Uuid.parse(verein.id),
- nummer = verein.oepsNr ?: "",
- name = verein.name,
- standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
- logo = null // Hier könnte später ein Logo-Service greifen
- )
+ try {
+ val verein = vereinRepository.findByOepsNr(oepsNr)
+ if (verein != null) {
+ // Robustes Parsing für Mock-Daten (z. B. "v1")
+ val uuid = try {
+ Uuid.parse(verein.id)
+ } catch (_: Exception) {
+ // Fallback für Mock-IDs während der Entwicklung
+ Uuid.random()
+ }
+
+ setVeranstalter(
+ id = uuid,
+ nummer = verein.oepsNr ?: "",
+ name = verein.name,
+ standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
+ logo = null
+ )
+ } else if (oepsNr.length >= 3) {
+ // Suche in den ZNS-Stammdaten als Fallback
+ znsImportProvider.searchRemote(oepsNr)
+ state = state.copy(znsSearchResults = znsImportProvider.state.remoteResults)
+ }
+ } catch (e: Exception) {
+ state = state.copy(error = "Fehler bei der Veranstalter-Suche: ${e.message}")
}
}
}
+ fun selectZnsVerein(znsVerein: ZnsRemoteVerein) {
+ setVeranstalter(
+ id = Uuid.random(), // Neuer Veranstalter wird angelegt
+ nummer = znsVerein.oepsNummer,
+ name = znsVerein.name,
+ standardOrt = znsVerein.ort ?: "",
+ logo = null
+ )
+ }
+
fun nextStep() {
state = state.copy(
currentStep = when (state.currentStep) {
@@ -155,23 +187,13 @@ class VeranstaltungWizardViewModel(
state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end, logoUrl = logo)
}
- fun updateTurnier(index: Int, nummer: String, path: String?) {
- val newList = state.turniere.toMutableList()
- if (index in newList.indices) {
- newList[index] = newList[index].copy(nummer = nummer, ausschreibungPath = path)
- state = state.copy(turniere = newList)
- }
- }
-
- fun addTurnier() {
- state = state.copy(turniere = state.turniere + TurnierEntry())
+ fun addTurnier(nummer: String = "", pfad: String? = null) {
+ state = state.copy(turniere = state.turniere + TurnierEntry(nummer = nummer, ausschreibungPath = pfad))
}
fun removeTurnier(index: Int) {
- if (state.turniere.size > 1) {
- val newList = state.turniere.toMutableList().apply { removeAt(index) }
- state = state.copy(turniere = newList)
- }
+ val newList = state.turniere.toMutableList().apply { removeAt(index) }
+ state = state.copy(turniere = newList)
}
fun saveVeranstaltung() {
diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungenScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungenScreen.kt
index ea63dc05..f821ee69 100644
--- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungenScreen.kt
+++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungenScreen.kt
@@ -43,12 +43,12 @@ fun VeranstaltungenScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "Veranstaltungen - verwalten",
+ text = "Events - verwalten",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
MsButton(
- text = "Neue Veranstaltung",
+ text = "Neues Event",
onClick = onVeranstaltungNeu
)
}
@@ -119,7 +119,7 @@ fun VeranstaltungenScreen(
)
Spacer(Modifier.height(Dimens.SpacingM))
Text(
- "Keine Veranstaltungen gefunden.",
+ "Keine Events gefunden.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt
index 36452e48..ff2889be 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/DesktopApp.kt
@@ -38,7 +38,10 @@ fun DesktopApp() {
// DeviceInitialization-Check beim Start
LaunchedEffect(Unit) {
if (!DeviceInitializationSettingsManager.isConfigured()) {
+ println("[DesktopApp] Setup fehlt -> Umleitung zum DeviceInitialization")
nav.navigateToScreen(AppScreen.DeviceInitialization)
+ } else {
+ println("[DesktopApp] Setup vorhanden.")
}
}
@@ -47,22 +50,32 @@ fun DesktopApp() {
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
// Vision_03 Update: Wir starten mit DeviceInitialization
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization
- && currentScreen !is AppScreen.VeranstaltungVerwaltung
+ && currentScreen !is AppScreen.EventVerwaltung
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
- && currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
- && currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail
+ && currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.EventKonfig
+ && currentScreen !is AppScreen.EventProfil && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.TurnierNeu
- && currentScreen !is AppScreen.ReiterVerwaltung
- && currentScreen !is AppScreen.PferdVerwaltung
- && currentScreen !is AppScreen.VereinVerwaltung
+ && currentScreen !is AppScreen.ReiterVerwaltung && currentScreen !is AppScreen.Reiter
+ && currentScreen !is AppScreen.PferdVerwaltung && currentScreen !is AppScreen.Pferde
+ && currentScreen !is AppScreen.VereinVerwaltung && currentScreen !is AppScreen.Vereine
+ && currentScreen !is AppScreen.FunktionaerVerwaltung && currentScreen !is AppScreen.FunktionaerProfil
+ && currentScreen !is AppScreen.ReiterProfil
+ && currentScreen !is AppScreen.PferdProfil
+ && currentScreen !is AppScreen.VereinProfil
&& currentScreen !is AppScreen.StammdatenImport
&& currentScreen !is AppScreen.NennungsEingang
- && currentScreen !is AppScreen.VeranstaltungNeu
+ && currentScreen !is AppScreen.EventNeu
&& currentScreen !is AppScreen.ConnectivityCheck
+ && currentScreen !is AppScreen.Dashboard
) {
- LaunchedEffect(Unit) {
- // Standard: Start im DeviceInitialization
- nav.navigateToScreen(AppScreen.DeviceInitialization)
+ LaunchedEffect(currentScreen) {
+ if (!DeviceInitializationSettingsManager.isConfigured()) {
+ println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup")
+ nav.navigateToScreen(AppScreen.DeviceInitialization)
+ } else {
+ println("[DesktopApp] Nicht authentifiziert, aber konfiguriert -> Dashboard")
+ nav.navigateToScreen(AppScreen.EventVerwaltung)
+ }
}
}
@@ -70,7 +83,7 @@ fun DesktopApp() {
is AppScreen.Login -> LoginScreen(
viewModel = loginViewModel,
onLoginSuccess = {
- val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
+ val returnTo = screen.returnTo ?: AppScreen.EventVerwaltung
nav.navigateToScreen(returnTo)
},
onBack = { nav.navigateBack() },
@@ -84,7 +97,7 @@ fun DesktopApp() {
onBack = { nav.navigateBack() },
onLogout = {
authTokenManager.clearToken()
- nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
+ nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.EventVerwaltung))
},
isAuthenticated = authState.isAuthenticated
)
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt
index f9fd1d6d..a3712e6b 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/DesktopMainLayout.kt
@@ -45,13 +45,15 @@ fun DesktopMainLayout(
}
// Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort)
- LaunchedEffect(onboardingSettings) {
+ LaunchedEffect(currentScreen) {
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) {
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
onNavigate(AppScreen.DeviceInitialization)
} else if (onboardingSettings.isConfigured && currentScreen is AppScreen.DeviceInitialization) {
- println("[DesktopNav] Setup abgeschlossen -> Wechsel zum Dashboard")
- onNavigate(AppScreen.VeranstaltungVerwaltung)
+ // Falls wir konfiguriert sind, aber im Setup-Screen landen (z.B. durch manuellen Nav-Call),
+ // erlauben wir den Aufenthalt dort (für Edit), aber forcieren keinen Redirect zum Dashboard hier,
+ // da dies der Wizard am Ende selbst macht.
+ println("[DesktopNav] Setup vorhanden und im Setup-Screen.")
}
}
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt
index 9f5536e7..667652e5 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/ContentArea.kt
@@ -27,7 +27,8 @@ import at.mocode.frontend.features.pferde.presentation.PferdeScreen
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
import at.mocode.frontend.features.ping.presentation.PingScreen
import at.mocode.frontend.features.ping.presentation.PingViewModel
-import at.mocode.frontend.features.profile.presentation.ProfileOnboardingWizard
+import at.mocode.frontend.features.profile.presentation.ProfileOnboardingScreen
+import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
import at.mocode.frontend.features.profile.presentation.ProfileScreen
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
@@ -70,17 +71,18 @@ fun DesktopContentArea(
val authTokenManager = org.koin.core.context.GlobalContext.get().get()
authTokenManager.setToken(finalSettings.sharedKey)
onSettingsChange(finalSettings)
- onNavigate(AppScreen.VeranstaltungVerwaltung)
+ // nav.navigateToScreen(...) wird hier nicht direkt gerufen, sondern onNavigate
+ onNavigate(AppScreen.EventVerwaltung)
})
}
DeviceInitializationScreen(viewModel = viewModel)
}
- // Haupt-Zentrale: Veranstaltung-Verwaltung
- is AppScreen.VeranstaltungVerwaltung -> {
+ // Haupt-Zentrale: Event-Verwaltung
+ is AppScreen.EventVerwaltung -> {
VeranstaltungenScreen(
- onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
- onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
+ onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu) },
+ onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) }
)
}
@@ -91,6 +93,15 @@ fun DesktopContentArea(
)
}
+ // --- Profile Onboarding ---
+ is AppScreen.ProfileOnboarding -> {
+ val viewModel = koinViewModel()
+ ProfileOnboardingScreen(
+ viewModel = viewModel,
+ onFinish = { onNavigate(AppScreen.EventVerwaltung) }
+ )
+ }
+
// --- Pferde-Verwaltung & Profil ---
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
val viewModel = koinViewModel()
@@ -165,14 +176,14 @@ fun DesktopContentArea(
is AppScreen.VeranstalterProfil -> VeranstalterDetail(
veranstalterId = currentScreen.id,
onBack = onBack,
- onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
- onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungNeu) },
+ onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) },
+ onNeuVeranstaltung = { onNavigate(AppScreen.EventNeu) },
)
- // Neuer Flow: Veranstalter auswählen → Veranstaltung-Wizard
+ // Neuer Flow: Veranstalter auswählen → Event-Wizard
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
onBack = onBack,
- onWeiter = { _ -> onNavigate(AppScreen.VeranstaltungNeu) },
+ onWeiter = { _ -> onNavigate(AppScreen.EventNeu) },
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
)
@@ -186,12 +197,12 @@ fun DesktopContentArea(
VeranstalterDetail(
veranstalterId = vId,
onBack = onBack,
- onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
- onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
+ onZurVeranstaltung = { evtId -> onNavigate(AppScreen.EventProfil(vId, evtId)) },
+ onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(vId)) },
)
}
- is AppScreen.VeranstaltungKonfig -> {
+ is AppScreen.EventKonfig -> {
val vId = currentScreen.veranstalterId
VeranstaltungKonfigScreen(
veranstalterId = vId,
@@ -201,12 +212,12 @@ fun DesktopContentArea(
// val allEvents = Store.allEvents()
// val newId = (allEvents.maxOfOrNull { it.id } ?: 0L) + 1L
// ...
- onNavigate(AppScreen.VeranstaltungProfil(vId, 0L)) // Mock
+ onNavigate(AppScreen.EventProfil(vId, 0L)) // Mock
}
)
}
- is AppScreen.VeranstaltungProfil -> {
+ is AppScreen.EventProfil -> {
VeranstaltungProfilScreen(
veranstalterId = currentScreen.veranstalterId,
veranstaltungId = currentScreen.veranstaltungId,
@@ -223,7 +234,7 @@ fun DesktopContentArea(
)
}
- is AppScreen.VeranstaltungDetail -> {
+ is AppScreen.EventDetail -> {
val repository: at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository = koinInject()
VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id,
@@ -235,7 +246,7 @@ fun DesktopContentArea(
)
}
- is AppScreen.VeranstaltungNeu -> {
+ is AppScreen.EventNeu -> {
val viewModel: at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel = koinViewModel()
at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardScreen(
viewModel = viewModel,
@@ -323,18 +334,13 @@ fun DesktopContentArea(
ProfileScreen(viewModel = viewModel)
}
- is AppScreen.ProfileOnboarding -> {
- val viewModel = koinViewModel()
- ProfileOnboardingWizard(
- viewModel = viewModel,
- onFinish = { onNavigate(AppScreen.Dashboard) }
- )
- }
- is AppScreen.Home, is AppScreen.Dashboard -> {
+ is AppScreen.Home, is AppScreen.Dashboard, is AppScreen.PortalDashboard,
+ is AppScreen.Meisterschaften, is AppScreen.Cups,
+ is AppScreen.CreateTournament, is AppScreen.OrganizerProfile -> {
AdminUebersichtScreen(
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
- onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) }
+ onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.EventDetail(id)) }
)
}
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/NavRail.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/NavRail.kt
index 996b11e3..8598b74a 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/NavRail.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/NavRail.kt
@@ -35,21 +35,13 @@ fun DesktopNavRail(
icon = Icons.Default.Adjust,
label = "Logo",
selected = false,
- onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
+ onClick = { onNavigate(AppScreen.EventVerwaltung) },
enabled = isConfigured
)
Spacer(Modifier.height(Dimens.SpacingL))
// Navigations-Items
- NavRailItem(
- icon = Icons.Default.Dashboard,
- label = "Admin",
- selected = currentScreen is AppScreen.VeranstaltungVerwaltung || currentScreen is AppScreen.VeranstaltungDetail,
- onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
- enabled = isConfigured
- )
-
NavRailItem(
icon = Icons.Default.CloudDownload,
label = "ZNS-Import",
@@ -101,7 +93,7 @@ fun DesktopNavRail(
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
)
DropdownMenuItem(
- text = { Text("Richter") },
+ text = { Text("Funktionäre") },
onClick = {
showStammdatenMenu = false
onNavigate(AppScreen.FunktionaerVerwaltung)
@@ -111,6 +103,43 @@ fun DesktopNavRail(
}
}
+ var showVerwaltungMenu by remember { mutableStateOf(false) }
+ Box {
+ NavRailItem(
+ icon = Icons.Default.Dashboard,
+ label = "Verwaltungen",
+ selected = currentScreen is AppScreen.EventVerwaltung ||
+ currentScreen is AppScreen.EventDetail ||
+ currentScreen is AppScreen.VeranstalterVerwaltung ||
+ currentScreen is AppScreen.VeranstalterAuswahl,
+ onClick = { showVerwaltungMenu = true },
+ enabled = isConfigured
+ )
+
+ DropdownMenu(
+ expanded = showVerwaltungMenu && isConfigured,
+ onDismissRequest = { showVerwaltungMenu = false },
+ offset = DpOffset(Dimens.NavRailWidth, 0.dp)
+ ) {
+ DropdownMenuItem(
+ text = { Text("Veranstalter") },
+ onClick = {
+ showVerwaltungMenu = false
+ onNavigate(AppScreen.VeranstalterVerwaltung)
+ },
+ leadingIcon = { Icon(Icons.Default.Business, contentDescription = null) }
+ )
+ DropdownMenuItem(
+ text = { Text("Events") },
+ onClick = {
+ showVerwaltungMenu = false
+ onNavigate(AppScreen.EventVerwaltung)
+ },
+ leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
+ )
+ }
+ }
+
NavRailItem(
icon = Icons.Default.Email,
label = "Mails",
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt
index 7f366af1..72f102d9 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/layout/components/TopHeader.kt
@@ -43,7 +43,7 @@ fun DesktopTopHeader(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// Zurück-Button ausblenden auf Startseite oder im Setup
- if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.VeranstaltungVerwaltung) {
+ if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.EventVerwaltung) {
IconButton(
onClick = {
// Verhindere Rücksprung zum Setup, wenn konfiguriert
@@ -65,7 +65,7 @@ fun DesktopTopHeader(
// Home Icon als Anker
IconButton(
- onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
+ onClick = { onNavigate(AppScreen.EventVerwaltung) },
modifier = Modifier.size(Dimens.IconSizeM),
enabled = isConfigured
) {
@@ -207,7 +207,7 @@ private fun BreadcrumbContent(
)
}
- is AppScreen.VeranstaltungProfil -> {
+ is AppScreen.EventProfil -> {
BreadcrumbSeparator()
Text(
text = "Veranstalter-Verwaltung",
@@ -224,43 +224,43 @@ private fun BreadcrumbContent(
)
BreadcrumbSeparator()
Text(
- text = "Veranstaltung #${currentScreen.veranstaltungId}",
+ text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
- is AppScreen.VeranstaltungVerwaltung -> {
+ is AppScreen.EventVerwaltung -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltungs-Verwaltung",
+ text = "Event-Verwaltung",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
- is AppScreen.VeranstaltungDetail -> {
+ is AppScreen.EventDetail -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltungs-Verwaltung",
+ text = "Event-Verwaltung",
style = textStyle.copy(color = clickableColor),
- modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
+ modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
)
BreadcrumbSeparator()
Text(
- text = "Veranstaltung #${currentScreen.id}",
+ text = "Event #${currentScreen.id}",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
- is AppScreen.VeranstaltungNeu -> {
+ is AppScreen.EventNeu -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltungs-Verwaltung",
+ text = "Event-Verwaltung",
style = textStyle.copy(color = clickableColor),
- modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
+ modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
)
BreadcrumbSeparator()
Text(
- text = "Neue Veranstaltung",
+ text = "Neues Event",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
@@ -268,10 +268,10 @@ private fun BreadcrumbContent(
is AppScreen.TurnierDetail -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltung #${currentScreen.veranstaltungId}",
+ text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
- onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
+ onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -284,10 +284,10 @@ private fun BreadcrumbContent(
is AppScreen.TurnierNeu -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltung #${currentScreen.veranstaltungId}",
+ text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
- onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
+ onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -300,10 +300,10 @@ private fun BreadcrumbContent(
is AppScreen.Billing -> {
BreadcrumbSeparator()
Text(
- text = "Veranstaltung #${currentScreen.veranstaltungId}",
+ text = "Event #${currentScreen.veranstaltungId}",
style = textStyle.copy(color = clickableColor),
modifier = Modifier.clickable {
- onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
+ onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
},
)
BreadcrumbSeparator()
@@ -356,7 +356,7 @@ private fun BreadcrumbContent(
is AppScreen.FunktionaerVerwaltung -> {
BreadcrumbSeparator()
Text(
- text = "Richter-Verwaltung",
+ text = "Funktionär-Verwaltung",
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
)
}
diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/details/VeranstaltungDetails.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/details/VeranstaltungDetails.kt
index 99fbf8c4..be5c8626 100644
--- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/details/VeranstaltungDetails.kt
+++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/veranstaltung/details/VeranstaltungDetails.kt
@@ -4,7 +4,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.OpenInNew
-import androidx.compose.material.icons.filled.*
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Event
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Place
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -33,7 +36,7 @@ fun VeranstaltungProfilScreen(
val turniere = TurnierStore.list(veranstaltungId)
if (veranstaltung == null) {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Veranstaltung nicht gefunden") }
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Event nicht gefunden") }
return@DesktopTheme
}
@@ -65,7 +68,7 @@ fun VeranstaltungProfilScreen(
KpiCard("Ort", veranstaltung.ort, Icons.Default.Place, Modifier.weight(1f))
}
- Text("Turniere in dieser Veranstaltung", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ Text("Turniere in diesem Event", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
if (turniere.isEmpty()) {
Card(Modifier.fillMaxWidth()) {
Box(Modifier.padding(32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -81,7 +84,7 @@ fun VeranstaltungProfilScreen(
}
}
- // Rechte Spalte: Veranstalter Info & Aktionen
+ // Rechte Spalte: Veranstalter Information & Aktionen
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Card {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {