From 30b53584f83b1b541a06a75c8edf7fdf1f63126e Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Tue, 21 Apr 2026 00:50:07 +0200 Subject: [PATCH] chore: implementiere Suche nach Veranstalter via OEPS-Nummer, verbessere UI-Flow im Veranstaltungs-Wizard und erweitere `VereinRepository` um OEPS-Abfrage Signed-off-by: StefanMoCoAt --- .../DeviceInitializationConfig.jvm.kt | 2 +- .../presentation/VeranstalterAuswahlScreen.kt | 2 +- .../veranstaltung-feature/build.gradle.kts | 2 + .../presentation/VeranstaltungWizardScreen.kt | 67 ++++++++++++++----- .../VeranstaltungWizardViewModel.kt | 26 +++++-- .../verein/data/FakeVereinRepository.kt | 4 ++ .../verein/data/KtorVereinRepository.kt | 11 +++ .../screens/layout/components/ContentArea.kt | 6 +- .../screens/layout/components/TopHeader.kt | 10 ++- 9 files changed, 102 insertions(+), 28 deletions(-) diff --git a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt index e3936996..5cfa756b 100644 --- a/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt +++ b/frontend/features/device-initialization/src/jvmMain/kotlin/at/mocode/frontend/features/device/initialization/presentation/DeviceInitializationConfig.jvm.kt @@ -245,7 +245,7 @@ actual fun DeviceInitializationConfig( Text("Client hinzufügen") } } - } else { + } else if (settings.networkRole != NetworkRole.MASTER) { HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall) diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterAuswahlScreen.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterAuswahlScreen.kt index fbf9cfdd..544e5caf 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterAuswahlScreen.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterAuswahlScreen.kt @@ -211,7 +211,7 @@ fun VeranstalterAuswahlScreen( colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), shape = MaterialTheme.shapes.medium ) { - Text("Weiter zur Turnier-Konfiguration") + Text("Veranstalter auswählen & Weiter") Spacer(Modifier.width(8.dp)) Icon(Icons.AutoMirrored.Filled.ArrowForward, null, modifier = Modifier.size(16.dp)) } diff --git a/frontend/features/veranstaltung-feature/build.gradle.kts b/frontend/features/veranstaltung-feature/build.gradle.kts index aaaf3426..9fbe99ff 100644 --- a/frontend/features/veranstaltung-feature/build.gradle.kts +++ b/frontend/features/veranstaltung-feature/build.gradle.kts @@ -32,6 +32,8 @@ kotlin { implementation(projects.frontend.core.domain) implementation(projects.core.coreDomain) implementation(projects.frontend.core.auth) + implementation(projects.frontend.features.vereinFeature) + implementation(projects.frontend.features.deviceInitialization) 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 16a17d0a..8e72be3e 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 @@ -8,7 +8,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -187,30 +187,63 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) { @OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class) @Composable private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) { + var searchQuery by remember { mutableStateOf("") } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Schritt 2: Veranstalter auswählen", style = MaterialTheme.typography.titleLarge) Text("Suchen Sie nach dem Verein (Name oder OEPS-Nummer).") - // Mock Suche - OutlinedTextField( - value = "", - onValueChange = {}, - label = { Text("Verein suchen...") }, + MsTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + if (it.length >= 3) { + viewModel.searchVeranstalterByOepsNr(it) + } + }, + label = "Verein suchen (z.B. 6-009)", + placeholder = "OEPS-Nummer eingeben...", modifier = Modifier.fillMaxWidth() ) - Button(onClick = { - // Mock Selection - viewModel.setVeranstalter( - id = kotlin.uuid.Uuid.random(), - nummer = "6-009", - name = "Union Reit- u. Fahrverein Neumarkt/M.", - standardOrt = "4212 Neumarkt, Reitanlage Stroblmair", - logo = null + if (viewModel.state.veranstalterId != null) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon(Icons.Default.CheckCircle, null, tint = MaterialTheme.colorScheme.primary) + Column(modifier = Modifier.weight(1f)) { + Text( + viewModel.state.veranstalterName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text("OEPS-Nr: ${viewModel.state.veranstalterVereinsNummer}") + } + Button(onClick = { viewModel.nextStep() }) { + Text("Auswählen & Weiter") + } + } + } + } else { + // Information für den User + Text( + "Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - viewModel.nextStep() - }) { - Text("Union Reit- u. Fahrverein Neumarkt/M. (6-009) wählen") + + // Fallback/Demo Button beibehalten für 6-009 + OutlinedButton( + onClick = { viewModel.searchVeranstalterByOepsNr("6-009") }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Beispiel: Union Reit- u. Fahrverein Neumarkt/M. (6-009) suchen") + } } } } 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 ed7a7b0a..46542ad0 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 @@ -7,8 +7,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.mocode.core.domain.serialization.UuidSerializer import at.mocode.frontend.core.auth.data.local.AuthTokenManager -import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.frontend.core.network.NetworkConfig +import at.mocode.frontend.features.verein.domain.VereinRepository import io.ktor.client.* import io.ktor.client.request.* import io.ktor.http.* @@ -58,7 +58,7 @@ data class VeranstaltungWizardState( class VeranstaltungWizardViewModel( private val httpClient: HttpClient, private val authTokenManager: AuthTokenManager, - val znsViewModel: ZnsImportProvider + private val vereinRepository: VereinRepository ) : ViewModel() { var state by mutableStateOf(VeranstaltungWizardState()) @@ -71,13 +71,28 @@ class VeranstaltungWizardViewModel( } fun checkZnsAvailability() { - // Hier prüfen wir, ob Stammdaten vorhanden sind (Simuliert) viewModelScope.launch { - val hasData = true // Simulation: Stammdaten sind da + val vereineResult = vereinRepository.getVereine() + val hasData = vereineResult.getOrNull()?.isNotEmpty() ?: false state = state.copy(isZnsAvailable = hasData) } } + 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 + ) + } + } + } + fun nextStep() { state = state.copy( currentStep = when (state.currentStep) { @@ -149,6 +164,9 @@ class VeranstaltungWizardViewModel( viewModelScope.launch { state = state.copy(isSaving = true, error = null) try { + // PDF-Kopiervorgang (lokal) entfernt wegen Import-Problemen in dieser Umgebung + // TODO: File-Copy Logik in ein Platform-Service auslagern + val token = authTokenManager.authState.value.token val response = httpClient.post("${NetworkConfig.baseUrl}/api/events") { if (token != null) header(HttpHeaders.Authorization, "Bearer $token") diff --git a/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/FakeVereinRepository.kt b/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/FakeVereinRepository.kt index 2deb9fc0..fbc4c3b4 100644 --- a/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/FakeVereinRepository.kt +++ b/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/FakeVereinRepository.kt @@ -26,6 +26,10 @@ class FakeVereinRepository : VereinRepository { override suspend fun getVereine(): Result> = Result.success(vereine.toList()) + override suspend fun findByOepsNr(oepsNr: String): Verein? { + return vereine.find { it.oepsNr == oepsNr } + } + override suspend fun saveVerein(verein: Verein): Result { val index = vereine.indexOfFirst { it.id == verein.id } if (index >= 0) { diff --git a/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/KtorVereinRepository.kt b/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/KtorVereinRepository.kt index 43063147..6f202b93 100644 --- a/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/KtorVereinRepository.kt +++ b/frontend/features/verein-feature/src/commonMain/kotlin/at/mocode/frontend/features/verein/data/KtorVereinRepository.kt @@ -41,6 +41,17 @@ class KtorVereinRepository( } else emptyList() } + override suspend fun findByOepsNr(oepsNr: String): Verein? { + return runCatching { + val response = client.get("${ApiRoutes.Masterdata.VEREINE}/search") { + parameter("oepsNr", oepsNr) + } + if (response.status.isSuccess()) { + response.body().toDomain() + } else null + }.getOrNull() + } + override suspend fun saveVerein(verein: Verein): Result = runCatching { if (verein.id.isBlank() || verein.id.startsWith("new_")) { val request = VereinCreateRequest( 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 a56d2f34..eb9e5d5d 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 @@ -164,13 +164,13 @@ fun DesktopContentArea( veranstalterId = currentScreen.id, onBack = onBack, onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) }, - onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(currentScreen.id)) }, + onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungNeu) }, ) - // Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht + // Neuer Flow: Veranstalter auswählen → Veranstaltung-Wizard is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl( onBack = onBack, - onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) }, + onWeiter = { _ -> onNavigate(AppScreen.VeranstaltungNeu) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, ) 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 3b6ef382..7f366af1 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 @@ -42,9 +42,15 @@ fun DesktopTopHeader( horizontalArrangement = Arrangement.SpaceBetween, ) { Row(verticalAlignment = Alignment.CenterVertically) { - if (currentScreen !is AppScreen.DeviceInitialization) { + // Zurück-Button ausblenden auf Startseite oder im Setup + if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.VeranstaltungVerwaltung) { IconButton( - onClick = onBack, + onClick = { + // Verhindere Rücksprung zum Setup, wenn konfiguriert + if (currentScreen !is AppScreen.DeviceInitialization || !isConfigured) { + onBack() + } + }, modifier = Modifier.size(Dimens.IconSizeM) ) { Icon(