diff --git a/docs/99_Journal/2026-04-21_Veranstalter-Neu-Overhaul.md b/docs/99_Journal/2026-04-21_Veranstalter-Neu-Overhaul.md new file mode 100644 index 00000000..dfed4290 --- /dev/null +++ b/docs/99_Journal/2026-04-21_Veranstalter-Neu-Overhaul.md @@ -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] diff --git a/docs/ScreenShots/EventNeu-Details_2026-04-21_22-02.png b/docs/ScreenShots/EventNeu-Details_2026-04-21_22-02.png new file mode 100644 index 00000000..bcdbc40e Binary files /dev/null and b/docs/ScreenShots/EventNeu-Details_2026-04-21_22-02.png differ diff --git a/docs/ScreenShots/stammdaten-vereine_screen_2026-04-21_22-17.png b/docs/ScreenShots/stammdaten-vereine_screen_2026-04-21_22-17.png new file mode 100644 index 00000000..be943783 Binary files /dev/null and b/docs/ScreenShots/stammdaten-vereine_screen_2026-04-21_22-17.png differ diff --git a/docs/ScreenShots/veranstalterNeu_2026-04-21_22-29.png b/docs/ScreenShots/veranstalterNeu_2026-04-21_22-29.png new file mode 100644 index 00000000..1ac64abf Binary files /dev/null and b/docs/ScreenShots/veranstalterNeu_2026-04-21_22-29.png differ diff --git a/docs/ScreenShots/veranstalterNeu_2_2026-04-21_22-37.png b/docs/ScreenShots/veranstalterNeu_2_2026-04-21_22-37.png new file mode 100644 index 00000000..6ceb7902 Binary files /dev/null and b/docs/ScreenShots/veranstalterNeu_2_2026-04-21_22-37.png differ diff --git a/docs/ScreenShots/veranstalterProfil_2026-04-21_22-24.png b/docs/ScreenShots/veranstalterProfil_2026-04-21_22-24.png new file mode 100644 index 00000000..d19b6644 Binary files /dev/null and b/docs/ScreenShots/veranstalterProfil_2026-04-21_22-24.png differ diff --git a/docs/ScreenShots/veranstalterVerwaltung_2026-04-21_22-22.png b/docs/ScreenShots/veranstalterVerwaltung_2026-04-21_22-22.png new file mode 100644 index 00000000..e2fb29f5 Binary files /dev/null and b/docs/ScreenShots/veranstalterVerwaltung_2026-04-21_22-22.png differ diff --git a/docs/ScreenShots/zns-import_screen_2026-04-21_22-13.png b/docs/ScreenShots/zns-import_screen_2026-04-21_22-13.png new file mode 100644 index 00000000..4a5ed04e Binary files /dev/null and b/docs/ScreenShots/zns-import_screen_2026-04-21_22-13.png differ diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt index 1b53bdd7..b737a5bc 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt @@ -11,5 +11,5 @@ val veranstalterModule = module { single { FakeVeranstalterRepository() } factory { VeranstalterViewModel(get()) } factory { VeranstalterDetailViewModel(get()) } - factory { VeranstalterWizardViewModel(get()) } + factory { VeranstalterWizardViewModel(get(), get(), get()) } } diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterNeuScreen.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterNeuScreen.kt index 44f4489f..edea234b 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterNeuScreen.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterNeuScreen.kt @@ -1,220 +1,281 @@ 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons 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.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.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight 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.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 fun VeranstalterNeuScreen( - onAbbrechen: () -> Unit, - onSpeichern: (vereinsname: String, oepsNummer: String, email: String) -> Unit, + viewModel: VeranstalterWizardViewModel, + onAbbrechen: () -> Unit, + onFinish: () -> Unit ) { - var vereinsname by remember { mutableStateOf("") } - 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 state by viewModel.state.collectAsState() - val isValid = vereinsname.isNotBlank() && oepsNummer.isNotBlank() && - ansprechpartner.isNotBlank() && email.isNotBlank() - - 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), - ) + if (state.success) { + LaunchedEffect(Unit) { + onFinish() } - } } - Spacer(Modifier.height(24.dp)) - - // Formular-Card - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 40.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), - ) { - Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - - // --- Vereinsdaten --- - Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) - - MsTextField( - value = vereinsname, - onValueChange = { vereinsname = it }, - label = "Vereinsname *", - modifier = Modifier.fillMaxWidth(), - ) - - MsTextField( - value = oepsNummer, - onValueChange = { oepsNummer = it }, - label = "OEPS-Nummer *", - modifier = Modifier.fillMaxWidth(), - helperText = "Offizielle Vereinsnummer des OEPS" - ) - - HorizontalDivider() - - // --- Kontaktdaten --- - Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) - - MsTextField( - value = ansprechpartner, - onValueChange = { ansprechpartner = it }, - label = "Ansprechpartner *", - modifier = Modifier.fillMaxWidth(), - ) - - MsTextField( - value = email, - onValueChange = { email = it }, - label = "E-Mail *", - modifier = Modifier.fillMaxWidth(), - helperText = "Login-Daten werden an diese Adresse verschickt" - ) - - MsTextField( - value = telefon, - onValueChange = { telefon = it }, - label = "Telefon", - modifier = Modifier.fillMaxWidth(), - ) - - HorizontalDivider() - - // --- Adresse --- - Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp) - - MsTextField( - value = strasse, - onValueChange = { strasse = it }, - label = "Straße & Hausnummer", - modifier = Modifier.fillMaxWidth(), - ) - - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - MsTextField( - value = plz, - onValueChange = { plz = it }, - label = "PLZ", - modifier = Modifier.width(120.dp), - ) - MsTextField( - value = ort, - onValueChange = { ort = it }, - label = "Ort", - modifier = Modifier.weight(1f), - ) + Column(modifier = Modifier.fillMaxSize()) { + // Header + Row( + modifier = Modifier.padding(Dimens.SpacingL), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM) + ) { + IconButton(onClick = onAbbrechen) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + Column { + Text( + text = "+ Neuen Veranstalter", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + Text( + text = "Legen Sie einen neuen Veranstalter an. Nutzen Sie die Suche, um Daten aus den ZNS-Stammdaten zu übernehmen.", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + + Row( + modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL), + horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingL) + ) { + // Linke Spalte: Suche + Column(modifier = Modifier.weight(1f).fillMaxHeight()) { + Text( + "Stammdaten-Suche", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = Dimens.SpacingM) + ) + + // Vereinssuche + SearchSection( + label = "Verein suchen (ZNS)", + query = state.vereinSearchQuery, + onQueryChange = { viewModel.send(VeranstalterWizardIntent.SearchVerein(it)) }, + placeholder = "Name oder OEBS-Nr...", + isSearching = state.isSearchingVerein, + results = state.vereinSearchResults, + renderResult = { verein -> + ListItem( + headlineContent = { Text(verein.name) }, + supportingContent = { Text("${verein.oepsNummer} | ${verein.ort ?: "-"}") }, + leadingContent = { Icon(Icons.Default.Business, null) }, + modifier = Modifier.clickable { viewModel.send(VeranstalterWizardIntent.SelectVerein(verein)) } + ) + } + ) + + Spacer(Modifier.height(Dimens.SpacingL)) + + // Ansprechperson Suche + SearchSection( + label = "Ansprechperson suchen (Reiter)", + query = state.reiterSearchQuery, + onQueryChange = { viewModel.send(VeranstalterWizardIntent.SearchReiter(it)) }, + placeholder = "Name oder Satznummer...", + isSearching = state.isSearchingReiter, + results = state.reiterSearchResults, + renderResult = { reiter -> + ListItem( + headlineContent = { Text("${reiter.vorname} ${reiter.nachname}") }, + supportingContent = { Text("Satz: ${reiter.satznummer ?: "-"} | ${reiter.lizenz ?: "-"}") }, + leadingContent = { Icon(Icons.Default.Person, null) }, + modifier = Modifier.clickable { viewModel.send(VeranstalterWizardIntent.SelectReiter(reiter)) } + ) + } + ) + } + + // Rechte Spalte: Details & Vorschau + Column( + modifier = Modifier + .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 SearchSection( + label: String, + query: String, + onQueryChange: (String) -> Unit, + placeholder: String, + isSearching: Boolean, + results: List, + 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)) - } } diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt index 0272d446..d599abc3 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/presentation/VeranstalterWizardViewModel.kt @@ -1,6 +1,10 @@ package at.mocode.frontend.features.veranstalter.presentation 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.VeranstalterRepository import kotlinx.coroutines.CoroutineScope @@ -25,7 +29,15 @@ data class VeranstalterWizardState( val logoBase64: String? = null, val loginStatus: String = "Aktiv", val success: Boolean = false, - val errorMessage: String? = null + val errorMessage: String? = null, + + // Search & Populate + val vereinSearchQuery: String = "", + val vereinSearchResults: List = emptyList(), + val reiterSearchQuery: String = "", + val reiterSearchResults: List = emptyList(), + val isSearchingVerein: Boolean = false, + val isSearchingReiter: Boolean = false ) sealed interface VeranstalterWizardIntent { @@ -39,10 +51,18 @@ sealed interface VeranstalterWizardIntent { data class UpdateAdresse(val v: String) : VeranstalterWizardIntent data class UpdateLogo(val base64: String?) : 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( - private val repo: VeranstalterRepository + private val repo: VeranstalterRepository, + private val masterdataRepository: MasterdataRepository, + private val znsImportProvider: ZnsImportProvider ) : ViewModel() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val _state = MutableStateFlow(VeranstalterWizardState()) @@ -60,9 +80,63 @@ class VeranstalterWizardViewModel( is VeranstalterWizardIntent.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v) is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64) 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) { _state.value = _state.value.copy(isLoading = true, editId = id) scope.launch { 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 788d18e5..f44db4e0 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 @@ -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.TurnierWizard 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.verein.presentation.VereinScreen import at.mocode.frontend.features.verein.presentation.VereinViewModel @@ -198,11 +200,14 @@ fun DesktopContentArea( onNeu = { onNavigate(AppScreen.VeranstalterNeu) }, ) - is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard( - editId = null, - onCancel = onBack, - onVereinCreated = { newId: Long -> onNavigate(AppScreen.VeranstalterProfil(newId)) } - ) + is AppScreen.VeranstalterNeu -> { + val viewModel = koinViewModel() + VeranstalterNeuScreen( + viewModel = viewModel, + onAbbrechen = onBack, + onFinish = onBack + ) + } is AppScreen.VeranstalterDetail -> { val vId = currentScreen.veranstalterId diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/preview/ScreenPreviews.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/preview/ScreenPreviews.kt index 7a063604..09eeea75 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/preview/ScreenPreviews.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/preview/ScreenPreviews.kt @@ -1,6 +1,7 @@ package at.mocode.frontend.shell.desktop.screens.preview import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import at.mocode.frontend.core.designsystem.preview.ComponentPreview 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.presentation.* 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.zns.parser.ZnsBewerb import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter @@ -52,10 +56,7 @@ fun PreviewVeranstalterAuswahlScreen() { @Composable fun PreviewVeranstalterNeuScreen() { MaterialTheme { - VeranstalterNeuScreen( - onAbbrechen = {}, - onSpeichern = { _, _, _ -> }, - ) + Text("Vorschau deaktiviert - ViewModel benötigt") } }