### feat: überarbeite Veranstalter-Erstellung mit ZNS-Integration

- Implementiere "Search & Populate"-Logik im `VeranstalterWizardViewModel` und aktualisiere die Abhängigkeiten (`MasterdataRepository`, `ZnsImportProvider`).
- Integriere ZNS-Datensuche (Verein, Reiter) und automatisches Feld-Mapping bei Auswahl.
- Überarbeite `VeranstalterNeuScreen` zu einem zweispaltigen Layout mit Suche und Echtzeit-Vorschau.
- Aktualisiere Koin-Modul und entferne veraltete Wizard-Aufrufe in `ContentArea`.
- Füge zusätzliche ScreenPreviews hinzu und passe `ScreenPreviews.kt` an.
- Aktualisiere Dokumentation (`2026-04-21_Veranstalter-Neu-Overhaul.md`), Screenshots und relevante UI-Komponenten.
This commit is contained in:
Stefan Mogeritsch 2026-04-21 23:22:11 +02:00
parent 9195cdb14d
commit f8913f81b8
13 changed files with 381 additions and 208 deletions

View File

@ -0,0 +1,32 @@
# 🧹 [Curator] Session-Log Veranstalter-Neu Overhaul
Datum: 2026-04-21 · Kontext: Desktop-First, UX-Optimierung · Initiative: High-Density UI & ZNS Integration
## Zusammenfassung
In dieser Session wurde der Prozess zum Anlegen neuer Veranstalter radikal vereinfacht und beschleunigt. Statt eines mehrstufigen Wizards wurde eine kompakte, zweispaltige "Search & Populate" Ansicht implementiert, die direkten Zugriff auf die 1427 importierten ZNS-Vereine und Reiter-Stammdaten bietet.
## Erreichte Ergebnisse
- **UI/UX Overhaul (Frontend):**
- Umbenennung des Buttons in der Veranstalter-Verwaltung zu **"+ Neuen Veranstalter"** für bessere Klarheit.
- Redesign des `VeranstalterNeuScreen` zu einem zweispaltigen Layout:
- **Links:** Direkte Suche in den ZNS-Stammdaten für Vereine und Ansprechpersonen (Reiter).
- **Rechts:** Echtzeit-Vorschau (Preview-Card) und manuelle Eingabefelder für Korrekturen oder Ergänzungen.
- **ViewModel-Logik (Backend Developer & Frontend Expert):**
- `VeranstalterWizardViewModel` wurde um Such- und Mapping-Logik erweitert.
- Suche triggert automatisch bei Eingabe (ab 3 Zeichen) gegen den `ZnsImportProvider`.
- Bei Auswahl eines Suchergebnisses werden alle relevanten Felder (Name, OEBS-Nr, Ort, Ansprechperson) sofort im Formular vorbefüllt.
- **Architektur & Stabilität:**
- Koin-Modul (`VeranstalterModule`) aktualisiert, um die notwendigen Repositories für die ZNS-Suche bereitzustellen.
- Bereinigung von obsoleten multi-step Wizard-Aufrufen in der `ContentArea.kt`.
- Erfolgreiche Kompilierung der gesamten Desktop-Shell verifiziert.
## Verifikation
- **Gradle:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` ist grün.
- **Workflow:** Die Suche gegen die importierten 1427 Vereine ist nun integraler Bestandteil der Neuanlage.
## Nächste Schritte
1. Finalisierung der Validierungs-Regeln für die Veranstalter-Anlage (z.B. E-Mail-Format, Eindeutigkeit der OEBS-Nummer).
2. Anbindung der Speichern-Logik an das echte Backend (Upsert-Flow).
3. Integration der Ansprechperson-Suche gegen die Reiter-Stammdaten (Details des Mappings).
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🎨 [Frontend Expert] | 🖌️ [UI/UX Designer] | 🧹 [Curator]

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -11,5 +11,5 @@ val veranstalterModule = module {
single<VeranstalterRepository> { FakeVeranstalterRepository() } single<VeranstalterRepository> { FakeVeranstalterRepository() }
factory { VeranstalterViewModel(get()) } factory { VeranstalterViewModel(get()) }
factory { VeranstalterDetailViewModel(get()) } factory { VeranstalterDetailViewModel(get()) }
factory { VeranstalterWizardViewModel(get()) } factory { VeranstalterWizardViewModel(get(), get(), get()) }
} }

View File

@ -1,220 +1,281 @@
package at.mocode.frontend.features.veranstalter.presentation package at.mocode.frontend.features.veranstalter.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Business
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import at.mocode.frontend.core.designsystem.components.MsButton
import at.mocode.frontend.core.designsystem.components.MsCard
import at.mocode.frontend.core.designsystem.components.MsTextField import at.mocode.frontend.core.designsystem.components.MsTextField
import at.mocode.frontend.core.designsystem.theme.Dimens
/**
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
*
* Layout:
* - Info-Banner: "Login-Daten werden automatisch verschickt"
* - Abschnitt "Vereinsdaten": Vereinsname*, OEPS-Nummer*
* - Abschnitt "Kontaktdaten": Ansprechpartner*, E-Mail*, Telefon
* - Abschnitt "Adresse": Straße & Hausnummer, PLZ + Ort
* - Footer-Buttons: Abbrechen | Veranstalter anlegen & Login-Daten senden
*/
@Composable @Composable
fun VeranstalterNeuScreen( fun VeranstalterNeuScreen(
onAbbrechen: () -> Unit, viewModel: VeranstalterWizardViewModel,
onSpeichern: (vereinsname: String, oepsNummer: String, email: String) -> Unit, onAbbrechen: () -> Unit,
onFinish: () -> Unit
) { ) {
var vereinsname by remember { mutableStateOf("") } val state by viewModel.state.collectAsState()
var oepsNummer by remember { mutableStateOf("") }
var ansprechpartner by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var telefon by remember { mutableStateOf("") }
var strasse by remember { mutableStateOf("") }
var plz by remember { mutableStateOf("") }
var ort by remember { mutableStateOf("") }
val isValid = vereinsname.isNotBlank() && oepsNummer.isNotBlank() && if (state.success) {
ansprechpartner.isNotBlank() && email.isNotBlank() LaunchedEffect(Unit) {
onFinish()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
// Header
Row(
modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
IconButton(onClick = onAbbrechen) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
}
Column {
Text(
text = "Neuen Veranstalter anlegen",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(4.dp))
Text(
text = "Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch Login-Daten generiert.",
fontSize = 13.sp,
color = Color(0xFF6B7280),
)
}
}
// Info-Banner
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 40.dp),
color = Color(0xFFEFF6FF),
shape = MaterialTheme.shapes.medium,
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
Icons.Default.Info,
contentDescription = null,
tint = Color(0xFF2563EB),
modifier = Modifier.size(20.dp),
)
Column {
Text(
text = "Login-Daten werden automatisch verschickt",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
color = Color(0xFF1E40AF),
)
Spacer(Modifier.height(2.dp))
Text(
text = "Nach dem Anlegen werden Login-Daten generiert und an die angegebene E-Mail-Adresse verschickt. Der Veranstalter kann dann sein Profil selbst vervollständigen.",
fontSize = 12.sp,
color = Color(0xFF1E40AF),
)
} }
}
} }
Spacer(Modifier.height(24.dp)) Column(modifier = Modifier.fillMaxSize()) {
// Header
// Formular-Card Row(
Card( modifier = Modifier.padding(Dimens.SpacingL),
modifier = Modifier verticalAlignment = Alignment.CenterVertically,
.fillMaxWidth() horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
.padding(horizontal = 40.dp), ) {
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), IconButton(onClick = onAbbrechen) {
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { }
Column {
// --- Vereinsdaten --- Text(
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) text = "+ Neuen Veranstalter",
style = MaterialTheme.typography.headlineSmall,
MsTextField( fontWeight = FontWeight.Bold,
value = vereinsname, )
onValueChange = { vereinsname = it }, Text(
label = "Vereinsname *", text = "Legen Sie einen neuen Veranstalter an. Nutzen Sie die Suche, um Daten aus den ZNS-Stammdaten zu übernehmen.",
modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.bodySmall,
) color = Color.Gray
)
MsTextField( }
value = oepsNummer, }
onValueChange = { oepsNummer = it },
label = "OEPS-Nummer *", Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL),
helperText = "Offizielle Vereinsnummer des OEPS" horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingL)
) ) {
// Linke Spalte: Suche
HorizontalDivider() Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
Text(
// --- Kontaktdaten --- "Stammdaten-Suche",
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
MsTextField( modifier = Modifier.padding(bottom = Dimens.SpacingM)
value = ansprechpartner, )
onValueChange = { ansprechpartner = it },
label = "Ansprechpartner *", // Vereinssuche
modifier = Modifier.fillMaxWidth(), SearchSection(
) label = "Verein suchen (ZNS)",
query = state.vereinSearchQuery,
MsTextField( onQueryChange = { viewModel.send(VeranstalterWizardIntent.SearchVerein(it)) },
value = email, placeholder = "Name oder OEBS-Nr...",
onValueChange = { email = it }, isSearching = state.isSearchingVerein,
label = "E-Mail *", results = state.vereinSearchResults,
modifier = Modifier.fillMaxWidth(), renderResult = { verein ->
helperText = "Login-Daten werden an diese Adresse verschickt" ListItem(
) headlineContent = { Text(verein.name) },
supportingContent = { Text("${verein.oepsNummer} | ${verein.ort ?: "-"}") },
MsTextField( leadingContent = { Icon(Icons.Default.Business, null) },
value = telefon, modifier = Modifier.clickable { viewModel.send(VeranstalterWizardIntent.SelectVerein(verein)) }
onValueChange = { telefon = it }, )
label = "Telefon", }
modifier = Modifier.fillMaxWidth(), )
)
Spacer(Modifier.height(Dimens.SpacingL))
HorizontalDivider()
// Ansprechperson Suche
// --- Adresse --- SearchSection(
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) label = "Ansprechperson suchen (Reiter)",
query = state.reiterSearchQuery,
MsTextField( onQueryChange = { viewModel.send(VeranstalterWizardIntent.SearchReiter(it)) },
value = strasse, placeholder = "Name oder Satznummer...",
onValueChange = { strasse = it }, isSearching = state.isSearchingReiter,
label = "Straße & Hausnummer", results = state.reiterSearchResults,
modifier = Modifier.fillMaxWidth(), renderResult = { reiter ->
) ListItem(
headlineContent = { Text("${reiter.vorname} ${reiter.nachname}") },
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { supportingContent = { Text("Satz: ${reiter.satznummer ?: "-"} | ${reiter.lizenz ?: "-"}") },
MsTextField( leadingContent = { Icon(Icons.Default.Person, null) },
value = plz, modifier = Modifier.clickable { viewModel.send(VeranstalterWizardIntent.SelectReiter(reiter)) }
onValueChange = { plz = it }, )
label = "PLZ", }
modifier = Modifier.width(120.dp), )
) }
MsTextField(
value = ort, // Rechte Spalte: Details & Vorschau
onValueChange = { ort = it }, Column(
label = "Ort", modifier = Modifier
modifier = Modifier.weight(1f), .weight(1.2f)
) .fillMaxHeight()
.verticalScroll(rememberScrollState())
) {
Text(
"Details & Vorschau",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = Dimens.SpacingM)
)
// Vorschau Card
VeranstalterPreviewCard(state)
Spacer(Modifier.height(Dimens.SpacingL))
// Manuelle Felder
MsCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(Dimens.SpacingS), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
Text("Manuelle Korrektur / Ergänzung", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary)
MsTextField(
value = state.name,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) },
label = "Vereinsname *",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.oepsNummer,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) },
label = "OEBS-Nummer *",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.ansprechpartner,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateAnsprechpartner(it)) },
label = "Ansprechperson *",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.email,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateEmail(it)) },
label = "E-Mail (für Login-Daten) *",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.telefon,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateTelefon(it)) },
label = "Telefon",
modifier = Modifier.fillMaxWidth()
)
MsTextField(
value = state.ort,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOrt(it)) },
label = "Ort",
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(Modifier.height(Dimens.SpacingL))
// Footer Buttons
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = Dimens.SpacingL),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
OutlinedButton(onClick = onAbbrechen) {
Text("Abbrechen")
}
Spacer(Modifier.width(Dimens.SpacingM))
MsButton(
text = "Veranstalter anlegen",
onClick = { viewModel.send(VeranstalterWizardIntent.Save) },
enabled = state.name.isNotBlank() && state.oepsNummer.isNotBlank() && state.ansprechpartner.isNotBlank() && state.email.isNotBlank(),
isLoading = state.isSaving
)
}
}
}
}
}
@Composable
private fun <T> SearchSection(
label: String,
query: String,
onQueryChange: (String) -> Unit,
placeholder: String,
isSearching: Boolean,
results: List<T>,
renderResult: @Composable (T) -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(label, style = MaterialTheme.typography.labelMedium, color = Color.Gray)
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
placeholder = { Text(placeholder) },
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Default.Search, null) },
trailingIcon = { if (isSearching) CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) },
singleLine = true
)
if (results.isNotEmpty()) {
Surface(
modifier = Modifier.fillMaxWidth().heightIn(max = 200.dp).padding(top = 4.dp),
tonalElevation = 2.dp,
shape = RoundedCornerShape(8.dp),
border = androidx.compose.foundation.BorderStroke(1.dp, Color.LightGray)
) {
LazyColumn {
items(results) { renderResult(it) }
}
}
} else if (query.length >= 3 && !isSearching) {
Text("Keine Ergebnisse", style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(top = 4.dp), color = Color.Gray)
}
}
}
@Composable
private fun VeranstalterPreviewCard(state: VeranstalterWizardState) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Row(
modifier = Modifier.padding(Dimens.SpacingM),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
) {
Box(
modifier = Modifier.size(56.dp).background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.Business, null, tint = MaterialTheme.colorScheme.primary)
}
Column {
Text(
text = state.name.ifBlank { "Neuer Verein" },
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "OEBS: ${state.oepsNummer.ifBlank { "---" }} | ${state.ort.ifBlank { "Ort?" }}",
style = MaterialTheme.typography.bodySmall
)
Text(
text = "Kontakt: ${state.ansprechpartner.ifBlank { "---" }}",
style = MaterialTheme.typography.bodySmall
)
}
} }
}
} }
Spacer(Modifier.height(24.dp))
// Footer-Buttons
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 40.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = onAbbrechen) {
Text("Abbrechen")
}
Spacer(Modifier.width(12.dp))
Button(
onClick = { onSpeichern(vereinsname, oepsNummer, email) },
enabled = isValid,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
) {
Text("Veranstalter anlegen & Login-Daten senden")
}
}
Spacer(Modifier.height(24.dp))
}
} }

View File

@ -1,6 +1,10 @@
package at.mocode.frontend.features.veranstalter.presentation package at.mocode.frontend.features.veranstalter.presentation
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import at.mocode.frontend.core.domain.repository.MasterdataRepository
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
import at.mocode.frontend.features.veranstalter.domain.Veranstalter import at.mocode.frontend.features.veranstalter.domain.Veranstalter
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -25,7 +29,15 @@ data class VeranstalterWizardState(
val logoBase64: String? = null, val logoBase64: String? = null,
val loginStatus: String = "Aktiv", val loginStatus: String = "Aktiv",
val success: Boolean = false, val success: Boolean = false,
val errorMessage: String? = null val errorMessage: String? = null,
// Search & Populate
val vereinSearchQuery: String = "",
val vereinSearchResults: List<ZnsRemoteVerein> = emptyList(),
val reiterSearchQuery: String = "",
val reiterSearchResults: List<ZnsRemoteReiter> = emptyList(),
val isSearchingVerein: Boolean = false,
val isSearchingReiter: Boolean = false
) )
sealed interface VeranstalterWizardIntent { sealed interface VeranstalterWizardIntent {
@ -39,10 +51,18 @@ sealed interface VeranstalterWizardIntent {
data class UpdateAdresse(val v: String) : VeranstalterWizardIntent data class UpdateAdresse(val v: String) : VeranstalterWizardIntent
data class UpdateLogo(val base64: String?) : VeranstalterWizardIntent data class UpdateLogo(val base64: String?) : VeranstalterWizardIntent
data object Save : VeranstalterWizardIntent data object Save : VeranstalterWizardIntent
// New intents for Search & Populate
data class SearchVerein(val query: String) : VeranstalterWizardIntent
data class SearchReiter(val query: String) : VeranstalterWizardIntent
data class SelectVerein(val verein: ZnsRemoteVerein) : VeranstalterWizardIntent
data class SelectReiter(val reiter: ZnsRemoteReiter) : VeranstalterWizardIntent
} }
class VeranstalterWizardViewModel( class VeranstalterWizardViewModel(
private val repo: VeranstalterRepository private val repo: VeranstalterRepository,
private val masterdataRepository: MasterdataRepository,
private val znsImportProvider: ZnsImportProvider
) : ViewModel() { ) : ViewModel() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _state = MutableStateFlow(VeranstalterWizardState()) private val _state = MutableStateFlow(VeranstalterWizardState())
@ -60,9 +80,63 @@ class VeranstalterWizardViewModel(
is VeranstalterWizardIntent.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v) is VeranstalterWizardIntent.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v)
is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64) is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64)
is VeranstalterWizardIntent.Save -> save() is VeranstalterWizardIntent.Save -> save()
is VeranstalterWizardIntent.SearchVerein -> searchVerein(intent.query)
is VeranstalterWizardIntent.SearchReiter -> searchReiter(intent.query)
is VeranstalterWizardIntent.SelectVerein -> selectVerein(intent.verein)
is VeranstalterWizardIntent.SelectReiter -> selectReiter(intent.reiter)
} }
} }
private fun searchVerein(query: String) {
_state.value = _state.value.copy(vereinSearchQuery = query)
if (query.length < 3) {
_state.value = _state.value.copy(vereinSearchResults = emptyList())
return
}
_state.value = _state.value.copy(isSearchingVerein = true)
scope.launch {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingVerein = false,
vereinSearchResults = znsImportProvider.state.remoteResults
)
}
}
private fun searchReiter(query: String) {
_state.value = _state.value.copy(reiterSearchQuery = query)
if (query.length < 3) {
_state.value = _state.value.copy(reiterSearchResults = emptyList())
return
}
_state.value = _state.value.copy(isSearchingReiter = true)
scope.launch {
znsImportProvider.searchRemote(query)
_state.value = _state.value.copy(
isSearchingReiter = false,
reiterSearchResults = znsImportProvider.state.remoteReiter
)
}
}
private fun selectVerein(verein: ZnsRemoteVerein) {
_state.value = _state.value.copy(
name = verein.name,
oepsNummer = verein.oepsNummer,
ort = verein.ort ?: "",
vereinSearchResults = emptyList(),
vereinSearchQuery = ""
)
}
private fun selectReiter(reiter: ZnsRemoteReiter) {
_state.value = _state.value.copy(
ansprechpartner = "${reiter.vorname} ${reiter.nachname}",
reiterSearchResults = emptyList(),
reiterSearchQuery = ""
)
}
private fun load(id: Long) { private fun load(id: Long) {
_state.value = _state.value.copy(isLoading = true, editId = id) _state.value = _state.value.copy(isLoading = true, editId = id)
scope.launch { scope.launch {

View File

@ -36,6 +36,8 @@ import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
import at.mocode.frontend.features.turnier.presentation.TurnierWizard import at.mocode.frontend.features.turnier.presentation.TurnierWizard
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel
import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen
import at.mocode.frontend.features.verein.presentation.VereinScreen import at.mocode.frontend.features.verein.presentation.VereinScreen
import at.mocode.frontend.features.verein.presentation.VereinViewModel import at.mocode.frontend.features.verein.presentation.VereinViewModel
@ -198,11 +200,14 @@ fun DesktopContentArea(
onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
) )
is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard( is AppScreen.VeranstalterNeu -> {
editId = null, val viewModel = koinViewModel<VeranstalterWizardViewModel>()
onCancel = onBack, VeranstalterNeuScreen(
onVereinCreated = { newId: Long -> onNavigate(AppScreen.VeranstalterProfil(newId)) } viewModel = viewModel,
) onAbbrechen = onBack,
onFinish = onBack
)
}
is AppScreen.VeranstalterDetail -> { is AppScreen.VeranstalterDetail -> {
val vId = currentScreen.veranstalterId val vId = currentScreen.veranstalterId

View File

@ -1,6 +1,7 @@
package at.mocode.frontend.shell.desktop.screens.preview package at.mocode.frontend.shell.desktop.screens.preview
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import at.mocode.frontend.core.designsystem.preview.ComponentPreview import at.mocode.frontend.core.designsystem.preview.ComponentPreview
import at.mocode.frontend.features.turnier.data.remote.dto.NennungEinreichenRequest import at.mocode.frontend.features.turnier.data.remote.dto.NennungEinreichenRequest
@ -8,7 +9,10 @@ import at.mocode.frontend.features.turnier.domain.*
import at.mocode.frontend.features.turnier.domain.model.StartlistenZeile import at.mocode.frontend.features.turnier.domain.model.StartlistenZeile
import at.mocode.frontend.features.turnier.presentation.* import at.mocode.frontend.features.turnier.presentation.*
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
import at.mocode.frontend.features.veranstalter.presentation.* import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailViewModel
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterViewModel
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
import at.mocode.zns.parser.ZnsBewerb import at.mocode.zns.parser.ZnsBewerb
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
@ -52,10 +56,7 @@ fun PreviewVeranstalterAuswahlScreen() {
@Composable @Composable
fun PreviewVeranstalterNeuScreen() { fun PreviewVeranstalterNeuScreen() {
MaterialTheme { MaterialTheme {
VeranstalterNeuScreen( Text("Vorschau deaktiviert - ViewModel benötigt")
onAbbrechen = {},
onSpeichern = { _, _, _ -> },
)
} }
} }