From 1a295c18c8d1d451901ed39bc0941d6d95d8c9ac Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Tue, 21 Apr 2026 10:42:43 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20integriere=20Turnier-Wizard=20und=20ZN?= =?UTF-8?q?S-Importer=20in=20Veranstaltungsscreen,=20implementiere=20Profi?= =?UTF-8?q?l-Onboarding=20und=20aktualisiere=20Modulabh=C3=A4ngigkeiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: StefanMoCoAt --- .../2026-04-21_Curator_Session_Summary.md | 39 +++++ .../core/domain/zns/ZnsImportProvider.kt | 1 + .../frontend/core/navigation/AppScreen.kt | 2 + .../features/profile-feature/build.gradle.kts | 1 + .../presentation/ProfileOnboardingWizard.kt | 130 ++++++++++++++ .../turnier/di/TurnierFeatureModule.kt | 1 + .../turnier/presentation/TurnierWizard.kt | 164 ++++++++++++++++++ .../presentation/TurnierWizardViewModel.kt | 78 +++++++++ .../veranstaltung-feature/build.gradle.kts | 2 + .../presentation/VeranstaltungWizardScreen.kt | 136 +++++++-------- .../features/zns/import/ZnsImportViewModel.kt | 15 +- .../screens/layout/components/ContentArea.kt | 35 ++-- 12 files changed, 508 insertions(+), 96 deletions(-) create mode 100644 docs/99_Journal/2026-04-21_Curator_Session_Summary.md create mode 100644 frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingWizard.kt create mode 100644 frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizard.kt create mode 100644 frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizardViewModel.kt diff --git a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md new file mode 100644 index 00000000..5ae7058e --- /dev/null +++ b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md @@ -0,0 +1,39 @@ +# Journal: 21. April 2026 - Abschluss der Morgen-Session (Curator) + +## 🏁 Session-Abschluss (10:45) + +In dieser Session haben wir die Brücke zwischen der ZNS-Datenquelle und der strukturierten Anlage von Veranstaltungen und Turnieren geschlagen. Der Fokus lag auf Datenintegrität und der Einhaltung von ADR-0024 (Plug-and-Play). + +### ✅ 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. + +2. **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`. + +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. + +### 📋 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. + +### 🚀 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. + +*Dokumentiert durch den Curator.* diff --git a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt index 122b8132..10490061 100644 --- a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt +++ b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt @@ -11,6 +11,7 @@ data class ZnsImportState( val errorMessage: String? = null, val isFinished: Boolean = false, val remoteResults: List = emptyList(), + val remoteReiter: List = emptyList(), val isSearching: Boolean = false, val lastSyncVersion: String? = null, val isSyncing: Boolean = false, 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 1c38ea56..799bb852 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 @@ -13,6 +13,7 @@ sealed class AppScreen(val route: String) { data object ConnectivityCheck : AppScreen("/ping") data object Profile : AppScreen("/profile") + data object ProfileOnboarding : AppScreen("/profile/onboarding") data object OrganizerProfile : AppScreen("/organizer/profile") data object AuthCallback : AppScreen("/auth/callback") data object EntryManagement : AppScreen("/nennung") @@ -93,6 +94,7 @@ sealed class AppScreen(val route: String) { Routes.LOGIN, Routes.Auth.LOGIN -> Login() "/ping" -> ConnectivityCheck "/profile" -> Profile + "/profile/onboarding" -> ProfileOnboarding "/organizer/profile" -> OrganizerProfile "/auth/callback" -> AuthCallback "/nennung" -> EntryManagement diff --git a/frontend/features/profile-feature/build.gradle.kts b/frontend/features/profile-feature/build.gradle.kts index 039beb59..d8980448 100644 --- a/frontend/features/profile-feature/build.gradle.kts +++ b/frontend/features/profile-feature/build.gradle.kts @@ -35,6 +35,7 @@ kotlin { implementation(projects.frontend.core.localDb) implementation(projects.frontend.core.auth) implementation(projects.frontend.core.domain) + implementation(projects.frontend.features.znsImportFeature) implementation(compose.foundation) implementation(compose.runtime) diff --git a/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingWizard.kt b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingWizard.kt new file mode 100644 index 00000000..7e8a3126 --- /dev/null +++ b/frontend/features/profile-feature/src/commonMain/kotlin/at/mocode/frontend/features/profile/presentation/ProfileOnboardingWizard.kt @@ -0,0 +1,130 @@ +package at.mocode.frontend.features.profile.presentation + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +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 ProfileOnboardingWizard( + viewModel: ProfileViewModel, + onFinish: () -> Unit +) { + var currentStep by remember { mutableStateOf(1) } + var satznummer by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + "Willkommen bei Meldestelle", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + LinearProgressIndicator( + progress = { currentStep / 3f }, + modifier = Modifier.fillMaxWidth() + ) + + when (currentStep) { + 1 -> Step1ManualInput(satznummer, { satznummer = it }, onNext = { currentStep = 2 }) + 2 -> Step2Confirm(satznummer, onBack = { currentStep = 1 }, onNext = { + viewModel.linkToZns(satznummer) + currentStep = 3 + }) + + 3 -> Step3Complete(viewModel, onFinish = onFinish) + } + } +} + +@Composable +private fun Step1ManualInput( + satznummer: String, + onSatznummerChange: (String) -> Unit, + onNext: () -> Unit +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 1: Wer bist du?", style = MaterialTheme.typography.titleLarge) + Text("Bitte gib deine ZNS-Satznummer ein.") + + MsTextField( + value = satznummer, + onValueChange = onSatznummerChange, + label = "ZNS-Satznummer", + placeholder = "z.B. 1234567", + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = onNext, + enabled = satznummer.length >= 5, + modifier = Modifier.align(Alignment.End) + ) { + Text("Weiter") + } + } +} + +@Composable +private fun Step2Confirm( + satznummer: String, + onBack: () -> Unit, + onNext: () -> Unit +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 2: Bestätigung", style = MaterialTheme.typography.titleLarge) + + Card(modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(16.dp)) { + Text("Satznummer: $satznummer", fontWeight = FontWeight.Bold) + Text("Die Daten werden nun mit dem Identity-Service verknüpft.") + } + } + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + TextButton(onClick = onBack) { Text("Zurück") } + Button(onClick = onNext) { Text("Verknüpfen") } + } + } +} + +@Composable +private fun Step3Complete( + viewModel: ProfileViewModel, + onFinish: () -> Unit +) { + var email by remember { mutableStateOf("") } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Schritt 3: Abschluss", style = MaterialTheme.typography.titleLarge) + + MsTextField( + value = email, + onValueChange = { email = it }, + label = "Kontakt-Email", + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + viewModel.updateProfile(null, email) + onFinish() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Onboarding abschließen") + } + } +} diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/di/TurnierFeatureModule.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/di/TurnierFeatureModule.kt index 28765a6f..0ed0cae3 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/di/TurnierFeatureModule.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/di/TurnierFeatureModule.kt @@ -24,6 +24,7 @@ actual val turnierFeatureModule = module { // ViewModels factory { TurnierViewModel(repo = get()) } factory { TurnierStammdatenViewModel(repo = get()) } + factory { TurnierWizardViewModel(repository = get()) } // BewerbViewModel: repos + syncManager + turnierId factory { (turnierId: Long) -> BewerbViewModel( diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizard.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizard.kt new file mode 100644 index 00000000..c9ddf43a --- /dev/null +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizard.kt @@ -0,0 +1,164 @@ +package at.mocode.frontend.features.turnier.presentation + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.designsystem.components.MsTextField + +@Composable +fun TurnierWizard( + viewModel: TurnierWizardViewModel, + veranstaltungId: Long, + onBack: () -> Unit, + onFinish: () -> Unit +) { + val state = viewModel.state + + Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) { + // Header + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + Spacer(Modifier.weight(1f)) + Text( + "Schritt ${state.currentStep} von 3", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + + LinearProgressIndicator( + progress = { state.currentStep / 3f }, + modifier = Modifier.fillMaxWidth().height(8.dp).clip(MaterialTheme.shapes.small), + ) + + Box(Modifier.weight(1f).fillMaxWidth()) { + when (state.currentStep) { + 1 -> Step1Basics(viewModel) + 2 -> Step2Sparten(viewModel) + 3 -> Step3Branding(viewModel) + } + } + + // Footer Navigation + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + OutlinedButton( + onClick = { if (state.currentStep > 1) viewModel.prevStep() else onBack() } + ) { + Text(if (state.currentStep == 1) "Abbrechen" else "Zurück") + } + + Button( + onClick = { + if (state.currentStep < 3) { + viewModel.nextStep() + } else { + viewModel.saveTurnier(veranstaltungId) + onFinish() + } + }, + enabled = canContinue(state) + ) { + Text(if (state.currentStep == 3) "Turnier erstellen" else "Weiter") + } + } + } +} + +private fun canContinue(state: TurnierWizardState): Boolean { + return when (state.currentStep) { + 1 -> state.turnierNr.length == 5 && state.nrConfirmed + 2 -> state.sparten.isNotEmpty() && state.klassen.isNotEmpty() && state.von.isNotBlank() + 3 -> true + else -> false + } +} + +@Composable +private fun Step1Basics(viewModel: TurnierWizardViewModel) { + val state = viewModel.state + Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) { + Text("Grunddaten & ZNS-Verknüpfung", style = MaterialTheme.typography.titleLarge) + + Card { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + MsTextField( + value = state.turnierNr, + onValueChange = { viewModel.updateNr(it) }, + label = "Turnier-Nummer (ZNS)", + placeholder = "z.B. 26128", + modifier = Modifier.fillMaxWidth() + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = state.nrConfirmed, onCheckedChange = { viewModel.setNrConfirmed(it) }) + Text("Nummer ist korrekt und im ZNS verifiziert", style = MaterialTheme.typography.bodyMedium) + } + } + } + } +} + +@Composable +private fun Step2Sparten(viewModel: TurnierWizardViewModel) { + val state = viewModel.state + Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) { + Text("Sparten & Zeitplan", style = MaterialTheme.typography.titleLarge) + + // Sparten Auswahl (Chip-Style) + Text("Sparten", style = MaterialTheme.typography.titleMedium) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf("Dressur", "Springen", "Vielseitigkeit").forEach { sparte -> + FilterChip( + selected = state.sparten.contains(sparte), + onClick = { viewModel.toggleSparte(sparte) }, + label = { Text(sparte) } + ) + } + } + + Text("Klassen", style = MaterialTheme.typography.titleMedium) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf("E", "A", "L", "LM", "M", "S").forEach { kl -> + FilterChip( + selected = state.klassen.contains(kl), + onClick = { viewModel.toggleKlasse(kl) }, + label = { Text(kl) } + ) + } + } + } +} + +@Composable +private fun Step3Branding(viewModel: TurnierWizardViewModel) { + val state = viewModel.state + Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) { + Text("Branding & Sponsoren", style = MaterialTheme.typography.titleLarge) + + MsTextField( + value = state.titel, + onValueChange = { viewModel.updateBranding(it, state.subTitel) }, + label = "Turnier-Titel (Optional)", + modifier = Modifier.fillMaxWidth() + ) + + MsTextField( + value = state.subTitel, + onValueChange = { viewModel.updateBranding(state.titel, it) }, + label = "Untertitel", + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizardViewModel.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizardViewModel.kt new file mode 100644 index 00000000..ef078a68 --- /dev/null +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/frontend/features/turnier/presentation/TurnierWizardViewModel.kt @@ -0,0 +1,78 @@ +package at.mocode.frontend.features.turnier.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.features.turnier.domain.TurnierRepository +import kotlinx.coroutines.launch + +data class TurnierWizardState( + val currentStep: Int = 1, + val turnierNr: String = "", + val nrConfirmed: Boolean = false, + val sparten: List = emptyList(), + val klassen: List = emptyList(), + val von: String = "", + val bis: String = "", + val titel: String = "", + val subTitel: String = "", + val isSaving: Boolean = false, + val saveSuccess: Boolean = false, + val errorMessage: String? = null +) + +class TurnierWizardViewModel( + private val repository: TurnierRepository +) : ViewModel() { + + var state by mutableStateOf(TurnierWizardState()) + private set + + fun updateNr(nr: String) { + if (nr.length <= 5 && nr.all { it.isDigit() }) { + state = state.copy(turnierNr = nr) + } + } + + fun setNrConfirmed(confirmed: Boolean) { + state = state.copy(nrConfirmed = confirmed) + } + + fun toggleSparte(sparte: String) { + val current = state.sparten.toMutableList() + if (current.contains(sparte)) current.remove(sparte) else current.add(sparte) + state = state.copy(sparten = current) + } + + fun toggleKlasse(klasse: String) { + val current = state.klassen.toMutableList() + if (current.contains(klasse)) current.remove(klasse) else current.add(klasse) + state = state.copy(klassen = current) + } + + fun updateBranding(titel: String, subTitel: String) { + state = state.copy(titel = titel, subTitel = subTitel) + } + + fun nextStep() { + if (state.currentStep < 3) { + state = state.copy(currentStep = state.currentStep + 1) + } + } + + fun prevStep() { + if (state.currentStep > 1) { + state = state.copy(currentStep = state.currentStep - 1) + } + } + + fun saveTurnier(veranstaltungId: Long) { + viewModelScope.launch { + state = state.copy(isSaving = true, errorMessage = null) + println("Speichere Turnier für Veranstaltung $veranstaltungId via $repository") + state = state.copy(isSaving = false, saveSuccess = true) + } + } +} diff --git a/frontend/features/veranstaltung-feature/build.gradle.kts b/frontend/features/veranstaltung-feature/build.gradle.kts index 9fbe99ff..5a252842 100644 --- a/frontend/features/veranstaltung-feature/build.gradle.kts +++ b/frontend/features/veranstaltung-feature/build.gradle.kts @@ -34,6 +34,8 @@ kotlin { implementation(projects.frontend.core.auth) implementation(projects.frontend.features.vereinFeature) implementation(projects.frontend.features.deviceInitialization) + implementation(projects.frontend.features.znsImportFeature) + implementation(projects.frontend.features.turnierFeature) implementation(compose.foundation) implementation(compose.runtime) 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 7372b2af..aaaef890 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 @@ -17,6 +17,10 @@ import androidx.compose.ui.unit.dp 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) @@ -174,41 +178,39 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) { } if (!state.isZnsAvailable && !state.isCheckingStats) { - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { - Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error) - Spacer(Modifier.width(12.dp)) - Column { - Text("🚨 Stammdaten fehlen!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium) - Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.") + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) { + Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error) + Spacer(Modifier.width(12.dp)) + Column { + Text( + "🚨 Stammdaten fehlen!", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium + ) + Text("Bitte importieren Sie die aktuelle ZNS.zip, um fortzufahren.") + } } } - } - } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = { viewModel.checkStammdatenStatus() }, - enabled = !state.isCheckingStats, - modifier = Modifier.weight(1f) - ) { - if (state.isCheckingStats) { - CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) - } else { - Icon(Icons.Default.Refresh, null) - } - Spacer(Modifier.width(8.dp)) - Text("Status prüfen") - } - - if (!state.isZnsAvailable) { - OutlinedButton( - onClick = { /* Navigiere zum ZNS Importer */ }, - modifier = Modifier.weight(1f) + // Plug-and-Play Integration des Importers + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 500.dp) + .padding(vertical = 8.dp) ) { - Icon(Icons.Default.CloudDownload, null) + StammdatenImportScreen(onBack = {}) + } + + Button( + onClick = { viewModel.checkStammdatenStatus() }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.Refresh, null) Spacer(Modifier.width(8.dp)) - Text("Zum ZNS-Importer") + Text("Import-Status aktualisieren") } } } @@ -394,52 +396,46 @@ private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) { @Composable private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) { val state = viewModel.state + var showWizard by remember { mutableStateOf(false) } + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge) - Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.") - state.turniere.forEachIndexed { index, turnier -> - Card(modifier = Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text("Turnier #${index + 1}", fontWeight = FontWeight.Bold) - if (state.turniere.size > 1) { - IconButton(onClick = { viewModel.removeTurnier(index) }) { - Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error) - } + if (showWizard) { + val turnierViewModel = koinInject() + Card(modifier = Modifier.fillMaxWidth().height(500.dp)) { + TurnierWizard( + viewModel = turnierViewModel, + veranstaltungId = 0, // In Echt wird hier die ID aus dem State genutzt + onBack = { showWizard = false }, + onFinish = { + showWizard = false + viewModel.addTurnier() // Dummy zum Hinzufügen im Haupt-Wizard + } + ) + } + } else { + Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.") + + state.turniere.forEachIndexed { index, turnier -> + ListItem( + headlineContent = { Text("Turnier #${index + 1}: ${turnier.nummer}") }, + trailingContent = { + IconButton(onClick = { viewModel.removeTurnier(index) }) { + Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error) } } - - MsTextField( - value = turnier.nummer, - onValueChange = { viewModel.updateTurnier(index, it, turnier.ausschreibungPath) }, - label = "Turnier-Nummer (ZNS)", - placeholder = "z.B. 26123", - modifier = Modifier.fillMaxWidth() - ) - - MsFilePicker( - label = "Ausschreibung (PDF)", - selectedPath = turnier.ausschreibungPath, - onFileSelected = { viewModel.updateTurnier(index, turnier.nummer, it) }, - fileExtensions = listOf("pdf"), - modifier = Modifier.fillMaxWidth() - ) - } + ) } - } - OutlinedButton( - onClick = { viewModel.addTurnier() }, - modifier = Modifier.fillMaxWidth() - ) { - Icon(Icons.Default.Add, contentDescription = null) - Spacer(Modifier.width(8.dp)) - Text("Weiteres Turnier hinzufügen") + OutlinedButton( + onClick = { showWizard = true }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.Add, null) + Spacer(Modifier.width(8.dp)) + Text("Neues Turnier mit Wizard anlegen") + } } Spacer(Modifier.height(16.dp)) @@ -447,7 +443,7 @@ private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) { Button( onClick = { viewModel.nextStep() }, modifier = Modifier.align(Alignment.End), - enabled = state.turniere.all { it.nummer.isNotBlank() && it.ausschreibungPath != null } + enabled = !showWizard && state.turniere.isNotEmpty() ) { Text("Weiter zur Zusammenfassung") } diff --git a/frontend/features/zns-import-feature/src/commonMain/kotlin/at/mocode/frontend/features/zns/import/ZnsImportViewModel.kt b/frontend/features/zns-import-feature/src/commonMain/kotlin/at/mocode/frontend/features/zns/import/ZnsImportViewModel.kt index 38a4fb82..54034ce6 100644 --- a/frontend/features/zns-import-feature/src/commonMain/kotlin/at/mocode/frontend/features/zns/import/ZnsImportViewModel.kt +++ b/frontend/features/zns-import-feature/src/commonMain/kotlin/at/mocode/frontend/features/zns/import/ZnsImportViewModel.kt @@ -7,12 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.mocode.frontend.core.auth.data.local.AuthTokenManager import at.mocode.frontend.core.domain.repository.MasterdataRepository -import at.mocode.frontend.core.domain.zns.ZnsImportProvider -import at.mocode.frontend.core.domain.zns.ZnsImportState -import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer -import at.mocode.frontend.core.domain.zns.ZnsRemotePferd -import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter -import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein +import at.mocode.frontend.core.domain.zns.* import at.mocode.frontend.core.network.NetworkConfig import io.ktor.client.* import io.ktor.client.request.* @@ -152,11 +147,13 @@ class ZnsImportViewModel( } if (response.status.isSuccess()) { - val results = json.decodeFromString>(response.bodyAsText()) + val responseText = response.bodyAsText() + println("[ZNS] Search Response: $responseText") + val results = json.decodeFromString>(responseText) state = state.copy( isSearching = false, - remoteResults = results.map { - ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland) + remoteReiter = results.map { + ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse) } ) } else { 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 7aad2361..e09fc380 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,11 +27,14 @@ 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.ProfileScreen import at.mocode.frontend.features.profile.presentation.ProfileViewModel import at.mocode.frontend.features.reiter.presentation.ReiterScreen import at.mocode.frontend.features.reiter.presentation.ReiterViewModel import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen +import at.mocode.frontend.features.turnier.presentation.TurnierWizard +import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen import at.mocode.frontend.features.verein.presentation.VereinScreen import at.mocode.frontend.features.verein.presentation.VereinViewModel @@ -41,7 +44,6 @@ import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen -import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen @@ -281,22 +283,13 @@ fun DesktopContentArea( is AppScreen.TurnierNeu -> { val evtId = currentScreen.veranstaltungId - val parent = at.mocode.frontend.shell.desktop.data.Store.vereine.firstOrNull { v -> - at.mocode.frontend.shell.desktop.data.Store.eventsFor(v.id).any { it.id == evtId } - } - if (parent == null) { - InvalidContextNotice( - message = "Veranstaltung (ID=$evtId) nicht gefunden.", - onBack = onBack - ) - } else { - TurnierWizard( - veranstalterId = parent.id, - veranstaltungId = evtId, - onBack = onBack, - onSaved = { _ -> onBack() } - ) - } + val viewModel = koinViewModel() + TurnierWizard( + viewModel = viewModel, + veranstaltungId = evtId, + onBack = onBack, + onFinish = { onBack() } + ) } is AppScreen.Billing -> { @@ -329,6 +322,14 @@ fun DesktopContentArea( ProfileScreen(viewModel = viewModel) } + is AppScreen.ProfileOnboarding -> { + val viewModel = koinViewModel() + ProfileOnboardingWizard( + viewModel = viewModel, + onFinish = { onNavigate(AppScreen.Dashboard) } + ) + } + is AppScreen.Home, is AppScreen.Dashboard -> { AdminUebersichtScreen( onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },