chore: implementiere Suche nach Veranstalter via OEPS-Nummer, verbessere UI-Flow im Veranstaltungs-Wizard und erweitere VereinRepository um OEPS-Abfrage
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-21 00:50:07 +02:00
parent c1327f3186
commit 30b53584f8
9 changed files with 102 additions and 28 deletions
@@ -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)
@@ -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))
}
@@ -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)
@@ -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")
}
}
}
}
@@ -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")
@@ -26,6 +26,10 @@ class FakeVereinRepository : VereinRepository {
override suspend fun getVereine(): Result<List<Verein>> = Result.success(vereine.toList())
override suspend fun findByOepsNr(oepsNr: String): Verein? {
return vereine.find { it.oepsNr == oepsNr }
}
override suspend fun saveVerein(verein: Verein): Result<Verein> {
val index = vereine.indexOfFirst { it.id == verein.id }
if (index >= 0) {
@@ -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<VereinDto>().toDomain()
} else null
}.getOrNull()
}
override suspend fun saveVerein(verein: Verein): Result<Verein> = runCatching {
if (verein.id.isBlank() || verein.id.startsWith("new_")) {
val request = VereinCreateRequest(