### feat: füge Validierung und Fehler-Handling zur Veranstalter-Erstellung hinzu

- Implementiere Validierungslogik im `VeranstalterWizardViewModel` (Pflichtfelder, E-Mail-Format).
- Zeige Validierungsfehler direkt in der `VeranstalterNeuScreen` an.
- Erweiterung der State-Klasse um `errors` für direktes UI-Feedback.
This commit is contained in:
2026-04-22 00:06:42 +02:00
parent f8913f81b8
commit f18b002f4e
3 changed files with 38 additions and 8 deletions
@@ -15,6 +15,7 @@ In dieser Session wurde der Prozess zum Anlegen neuer Veranstalter radikal verei
- `VeranstalterWizardViewModel` wurde um Such- und Mapping-Logik erweitert. - `VeranstalterWizardViewModel` wurde um Such- und Mapping-Logik erweitert.
- Suche triggert automatisch bei Eingabe (ab 3 Zeichen) gegen den `ZnsImportProvider`. - 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. - Bei Auswahl eines Suchergebnisses werden alle relevanten Felder (Name, OEBS-Nr, Ort, Ansprechperson) sofort im Formular vorbefüllt.
- **Neu:** Implementierung einer robusten Validierungs-Logik (Pflichtfelder & E-Mail-Format) mit direktem UI-Feedback.
- **Architektur & Stabilität:** - **Architektur & Stabilität:**
- Koin-Modul (`VeranstalterModule`) aktualisiert, um die notwendigen Repositories für die ZNS-Suche bereitzustellen. - 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`. - Bereinigung von obsoleten multi-step Wizard-Aufrufen in der `ContentArea.kt`.
@@ -25,8 +26,8 @@ In dieser Session wurde der Prozess zum Anlegen neuer Veranstalter radikal verei
- **Workflow:** Die Suche gegen die importierten 1427 Vereine ist nun integraler Bestandteil der Neuanlage. - **Workflow:** Die Suche gegen die importierten 1427 Vereine ist nun integraler Bestandteil der Neuanlage.
## Nächste Schritte ## Nächste Schritte
1. Finalisierung der Validierungs-Regeln für die Veranstalter-Anlage (z.B. E-Mail-Format, Eindeutigkeit der OEBS-Nummer). 1. Anbindung der Speichern-Logik an das echte Backend (Upsert-Flow).
2. Anbindung der Speichern-Logik an das echte Backend (Upsert-Flow). 2. Integration der Ansprechperson-Suche gegen die Reiter-Stammdaten (Details des Mappings).
3. Integration der Ansprechperson-Suche gegen die Reiter-Stammdaten (Details des Mappings). 3. Finalisierung der Berechtigungs-Prüfung für den ZNS-Zugriff im Desktop-Client.
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🎨 [Frontend Expert] | 🖌️ [UI/UX Designer] | 🧹 [Curator] 🏗️ [Lead Architect] | 👷 [Backend Developer] | 🎨 [Frontend Expert] | 🖌️ [UI/UX Designer] | 🧹 [Curator]
@@ -146,25 +146,33 @@ fun VeranstalterNeuScreen(
value = state.name, value = state.name,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) }, onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) },
label = "Vereinsname *", label = "Vereinsname *",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
isError = state.errors.containsKey("name"),
errorMessage = state.errors["name"]
) )
MsTextField( MsTextField(
value = state.oepsNummer, value = state.oepsNummer,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) }, onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) },
label = "OEBS-Nummer *", label = "OEBS-Nummer *",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
isError = state.errors.containsKey("oeps"),
errorMessage = state.errors["oeps"]
) )
MsTextField( MsTextField(
value = state.ansprechpartner, value = state.ansprechpartner,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateAnsprechpartner(it)) }, onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateAnsprechpartner(it)) },
label = "Ansprechperson *", label = "Ansprechperson *",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
isError = state.errors.containsKey("ansprechpartner"),
errorMessage = state.errors["ansprechpartner"]
) )
MsTextField( MsTextField(
value = state.email, value = state.email,
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateEmail(it)) }, onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateEmail(it)) },
label = "E-Mail (für Login-Daten) *", label = "E-Mail (für Login-Daten) *",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
isError = state.errors.containsKey("email"),
errorMessage = state.errors["email"]
) )
MsTextField( MsTextField(
value = state.telefon, value = state.telefon,
@@ -37,7 +37,8 @@ data class VeranstalterWizardState(
val reiterSearchQuery: String = "", val reiterSearchQuery: String = "",
val reiterSearchResults: List<ZnsRemoteReiter> = emptyList(), val reiterSearchResults: List<ZnsRemoteReiter> = emptyList(),
val isSearchingVerein: Boolean = false, val isSearchingVerein: Boolean = false,
val isSearchingReiter: Boolean = false val isSearchingReiter: Boolean = false,
val errors: Map<String, String> = emptyMap()
) )
sealed interface VeranstalterWizardIntent { sealed interface VeranstalterWizardIntent {
@@ -160,7 +161,27 @@ class VeranstalterWizardViewModel(
} }
} }
private fun validate(): Boolean {
val errors = mutableMapOf<String, String>()
val s = _state.value
if (s.name.isBlank()) errors["name"] = "Vereinsname ist erforderlich"
if (s.oepsNummer.isBlank()) errors["oeps"] = "OEBS-Nummer ist erforderlich"
if (s.ansprechpartner.isBlank()) errors["ansprechpartner"] = "Ansprechperson ist erforderlich"
if (s.email.isBlank()) {
errors["email"] = "E-Mail ist erforderlich"
} else if (!s.email.contains("@") || !s.email.contains(".")) {
errors["email"] = "Ungültiges E-Mail Format"
}
_state.value = _state.value.copy(errors = errors)
return errors.isEmpty()
}
private fun save() { private fun save() {
if (!validate()) return
val s = _state.value val s = _state.value
_state.value = _state.value.copy(isSaving = true) _state.value = _state.value.copy(isSaving = true)
scope.launch { scope.launch {