### 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.
32
docs/99_Journal/2026-04-21_Veranstalter-Neu-Overhaul.md
Normal 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]
|
||||
BIN
docs/ScreenShots/EventNeu-Details_2026-04-21_22-02.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/ScreenShots/stammdaten-vereine_screen_2026-04-21_22-17.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/ScreenShots/veranstalterNeu_2026-04-21_22-29.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/ScreenShots/veranstalterNeu_2_2026-04-21_22-37.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/ScreenShots/veranstalterProfil_2026-04-21_22-24.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/ScreenShots/veranstalterVerwaltung_2026-04-21_22-22.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/ScreenShots/zns-import_screen_2026-04-21_22-13.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
|
|
@ -11,5 +11,5 @@ val veranstalterModule = module {
|
|||
single<VeranstalterRepository> { FakeVeranstalterRepository() }
|
||||
factory { VeranstalterViewModel(get()) }
|
||||
factory { VeranstalterDetailViewModel(get()) }
|
||||
factory { VeranstalterWizardViewModel(get()) }
|
||||
factory { VeranstalterWizardViewModel(get(), get(), get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ZnsRemoteVerein> = emptyList(),
|
||||
val reiterSearchQuery: String = "",
|
||||
val reiterSearchResults: List<ZnsRemoteReiter> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<VeranstalterWizardViewModel>()
|
||||
VeranstalterNeuScreen(
|
||||
viewModel = viewModel,
|
||||
onAbbrechen = onBack,
|
||||
onFinish = onBack
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||