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 09:27:35 +02:00
parent 30b53584f8
commit 7acd9ea4c2
17 changed files with 230 additions and 58 deletions
@@ -2,9 +2,10 @@ package at.mocode.veranstaltung.feature.di
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
val veranstaltungModule = module {
factory { VeranstaltungManagementViewModel(get()) }
factory { VeranstaltungWizardViewModel(get(), get(), get()) }
factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) }
}
@@ -11,7 +11,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.core.designsystem.components.MsFilePicker
@@ -140,44 +140,84 @@ private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Schritt 1: Stammdaten-Verfügbarkeit prüfen", style = MaterialTheme.typography.titleLarge)
if (!state.isZnsAvailable) {
// Stats Anzeige
state.stammdatenStats?.let { stats ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Stammdaten-Status", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Letzter Import:")
Text(stats.lastImport ?: "Nie", fontWeight = FontWeight.Medium)
}
HorizontalDivider(
modifier = Modifier.alpha(0.5f),
thickness = DividerDefaults.Thickness,
color = DividerDefaults.color
)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Vereine:")
Text("${stats.vereinCount}", fontWeight = FontWeight.Medium)
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Reiter:")
Text("${stats.reiterCount}", fontWeight = FontWeight.Medium)
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Pferde:")
Text("${stats.pferdCount}", fontWeight = FontWeight.Medium)
}
}
}
}
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("Für die Anlage einer Veranstaltung werden Vereins- und Reitdaten benötigt. Bitte importieren Sie die aktuelle ZNS.zip (VEREIN01, LIZENZ01).")
Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.")
}
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { /* Navigiere zum ZNS Importer */ },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
onClick = { viewModel.checkStammdatenStatus() },
enabled = !state.isCheckingStats,
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.CloudDownload, null)
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("Zum ZNS-Importer")
Text("Status prüfen")
}
OutlinedButton(
onClick = { viewModel.checkZnsAvailability() },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Refresh, null)
Spacer(Modifier.width(8.dp))
Text("Status erneut prüfen")
}
} else {
Card(colors = CardDefaults.cardColors(containerColor = Color(0xFFE8F5E9))) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Check, null, tint = Color(0xFF2E7D32))
Spacer(Modifier.width(12.dp))
Text("Stammdaten sind aktuell und verfügbar.", color = Color(0xFF2E7D32))
if (!state.isZnsAvailable) {
OutlinedButton(
onClick = { /* Navigiere zum ZNS Importer */ },
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.CloudDownload, null)
Spacer(Modifier.width(8.dp))
Text("Zum ZNS-Importer")
}
}
Button(onClick = { viewModel.nextStep() }) {
}
if (state.isZnsAvailable) {
Button(
onClick = { viewModel.nextStep() },
modifier = Modifier.fillMaxWidth()
) {
Text("Weiter zur Veranstalter-Wahl")
}
}
@@ -230,19 +270,36 @@ private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) {
}
}
} 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
)
// Fallback/Demo Button beibehalten für 6-009
OutlinedButton(
onClick = { viewModel.searchVeranstalterByOepsNr("6-009") },
modifier = Modifier.fillMaxWidth()
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Beispiel: Union Reit- u. Fahrverein Neumarkt/M. (6-009) suchen")
Text(
"Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Text("Verein nicht gefunden?", style = MaterialTheme.typography.labelLarge)
Button(
onClick = { /* Navigiere zu Veranstalter anlegen */ },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Icon(Icons.Default.Add, null)
Spacer(Modifier.width(8.dp))
Text("Diesen Verein als neuen Veranstalter anlegen")
}
// Fallback/Demo Button
OutlinedButton(
onClick = { viewModel.searchVeranstalterByOepsNr("6-009") }
) {
Text("Beispiel: 6-009 suchen")
}
}
}
}
@@ -7,6 +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.repository.MasterdataRepository
import at.mocode.frontend.core.domain.repository.MasterdataStats
import at.mocode.frontend.core.network.NetworkConfig
import at.mocode.frontend.features.verein.domain.VereinRepository
import io.ktor.client.*
@@ -51,14 +53,17 @@ data class VeranstaltungWizardState(
val isSaving: Boolean = false,
val error: String? = null,
val createdVeranstaltungId: Uuid? = null,
val isZnsAvailable: Boolean = false
val isZnsAvailable: Boolean = false,
val stammdatenStats: MasterdataStats? = null,
val isCheckingStats: Boolean = false
)
@OptIn(ExperimentalUuidApi::class)
class VeranstaltungWizardViewModel(
private val httpClient: HttpClient,
private val authTokenManager: AuthTokenManager,
private val vereinRepository: VereinRepository
private val vereinRepository: VereinRepository,
private val masterdataRepository: MasterdataRepository
) : ViewModel() {
var state by mutableStateOf(VeranstaltungWizardState())
@@ -66,6 +71,7 @@ class VeranstaltungWizardViewModel(
init {
checkZnsAvailability()
checkStammdatenStatus()
// Simulation eines Initial-Datums
state = state.copy(startDatum = LocalDate(2026, 4, 25), endDatum = LocalDate(2026, 4, 26))
}
@@ -78,6 +84,18 @@ class VeranstaltungWizardViewModel(
}
}
fun checkStammdatenStatus() {
viewModelScope.launch {
state = state.copy(isCheckingStats = true)
try {
val stats = masterdataRepository.getStats()
state = state.copy(stammdatenStats = stats, isZnsAvailable = stats.vereinCount > 0, isCheckingStats = false)
} catch (e: Exception) {
state = state.copy(isCheckingStats = false, error = "Fehler beim Laden der Stammdaten-Stats: ${e.message}")
}
}
}
fun searchVeranstalterByOepsNr(oepsNr: String) {
viewModelScope.launch {
val verein = vereinRepository.findByOepsNr(oepsNr)