From b07d5d7386cf7dddc0e84e0a72afd1928d55c7dd Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sun, 12 Apr 2026 16:00:30 +0200 Subject: [PATCH] =?UTF-8?q?Enhance=20edit=20dialogs=20with=20validation=20?= =?UTF-8?q?and=20error=20handling,=20implement=20dropdown=20searches=20for?= =?UTF-8?q?=20Funktion=C3=A4re,=20and=20update=20Masterdata=20logs=20with?= =?UTF-8?q?=20completion=20notes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-04-12_Masterdata_Editoren_Curator_Log.md | 4 +- .../presentation/MasterdataEditDialogs.kt | 94 ++++++-- .../presentation/TurnierOrganisationTab.kt | 207 ++++++++++++++++-- 3 files changed, 264 insertions(+), 41 deletions(-) diff --git a/docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md b/docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md index 522d523a..b5e3da1b 100644 --- a/docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md +++ b/docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md @@ -34,8 +34,8 @@ Erweiterung der Stammdaten-Infrastruktur um Schreibzugriffe (Detail-Editoren), F - **DI-Check:** Verifikation der `znsImportModule` Registrierung in `main.kt`. ## 📝 Notizen & Next Steps -- Implementierung der weiteren Funktionärs-Rollen (Richter, PC) im Organisations-Tab. -- Erweiterung der `MasterdataEditDialogs` um Validierungs-Feedback (z.B. OEPS-Formatprüfung). +- Implementierung der weiteren Funktionärs-Rollen (Richter, PC) im Organisations-Tab. ✓ (Abgeschlossen am 12.04.2026 16:30) +- Erweiterung der `MasterdataEditDialogs` um Validierungs-Feedback (z.B. OEPS-Formatprüfung). ✓ (Abgeschlossen am 12.04.2026 16:30) - Vorbereitung Phase 10.3: Series-Context (Cups/Meisterschaften). --- diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt index f1661382..0d57cf41 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt @@ -20,29 +20,52 @@ fun ReiterEditDialog( var verein by remember { mutableStateOf(reiter.verein ?: "") } var feiId by remember { mutableStateOf(reiter.feiId ?: "") } + val isVornameValid = vorname.isNotBlank() + val isNachnameValid = nachname.isNotBlank() + val isOepsValid = oepsNummer.isBlank() || oepsNummer.all { it.isDigit() || it.isLetter() } // Einfache Prüfung + AlertDialog( onDismissRequest = onDismiss, title = { Text("Reiter bearbeiten") }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField(value = vorname, onValueChange = { vorname = it }, label = { Text("Vorname") }) - OutlinedTextField(value = nachname, onValueChange = { nachname = it }, label = { Text("Nachname") }) - OutlinedTextField(value = oepsNummer, onValueChange = { oepsNummer = it }, label = { Text("OEPS-Nr.") }) + OutlinedTextField( + value = vorname, + onValueChange = { vorname = it }, + label = { Text("Vorname*") }, + isError = !isVornameValid + ) + OutlinedTextField( + value = nachname, + onValueChange = { nachname = it }, + label = { Text("Nachname*") }, + isError = !isNachnameValid + ) + OutlinedTextField( + value = oepsNummer, + onValueChange = { oepsNummer = it }, + label = { Text("OEPS-Nr.") }, + isError = !isOepsValid, + supportingText = { if(!isOepsValid) Text("Ungültiges Format") } + ) OutlinedTextField(value = verein, onValueChange = { verein = it }, label = { Text("Verein") }) OutlinedTextField(value = feiId, onValueChange = { feiId = it }, label = { Text("FEI-ID") }) } }, confirmButton = { - Button(onClick = { - onSave(reiter.copy( - vorname = vorname, - nachname = nachname, - oepsNummer = oepsNummer, - satznummer = oepsNummer, - verein = verein, - feiId = feiId - )) - }) { + Button( + onClick = { + onSave(reiter.copy( + vorname = vorname, + nachname = nachname, + oepsNummer = oepsNummer, + satznummer = oepsNummer, + verein = verein, + feiId = feiId + )) + }, + enabled = isVornameValid && isNachnameValid && isOepsValid + ) { Text("Speichern") } }, @@ -65,26 +88,49 @@ fun PferdEditDialog( var oepsNummer by remember { mutableStateOf(pferd.oepsNummer ?: "") } var geburtsjahr by remember { mutableStateOf(pferd.geburtsjahr?.toString() ?: "") } + val isNameValid = name.isNotBlank() + val isLebensnrValid = lebensnummer.isNotBlank() + val isJahrValid = geburtsjahr.isBlank() || (geburtsjahr.toIntOrNull() != null && geburtsjahr.length == 4) + AlertDialog( onDismissRequest = onDismiss, title = { Text("Pferd bearbeiten") }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }) - OutlinedTextField(value = lebensnummer, onValueChange = { lebensnummer = it }, label = { Text("Lebensnummer") }) + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name*") }, + isError = !isNameValid + ) + OutlinedTextField( + value = lebensnummer, + onValueChange = { lebensnummer = it }, + label = { Text("Lebensnummer*") }, + isError = !isLebensnrValid + ) OutlinedTextField(value = oepsNummer, onValueChange = { oepsNummer = it }, label = { Text("OEPS-Nr.") }) - OutlinedTextField(value = geburtsjahr, onValueChange = { geburtsjahr = it }, label = { Text("Geburtsjahr") }) + OutlinedTextField( + value = geburtsjahr, + onValueChange = { geburtsjahr = it }, + label = { Text("Geburtsjahr") }, + isError = !isJahrValid, + supportingText = { if(!isJahrValid) Text("4-stellige Jahreszahl") } + ) } }, confirmButton = { - Button(onClick = { - onSave(pferd.copy( - name = name, - lebensnummer = lebensnummer, - oepsNummer = oepsNummer, - geburtsjahr = geburtsjahr.toIntOrNull() - )) - }) { + Button( + onClick = { + onSave(pferd.copy( + name = name, + lebensnummer = lebensnummer, + oepsNummer = oepsNummer, + geburtsjahr = geburtsjahr.toIntOrNull() + )) + }, + enabled = isNameValid && isLebensnrValid && isJahrValid + ) { Text("Speichern") } }, diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt index ad97f498..0dcb2d5e 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt @@ -40,6 +40,15 @@ fun OrganisationTabContent(viewModel: NennungViewModel) { var schmied by remember { mutableStateOf("") } var steward by remember { mutableStateOf("") } + // --- Dropdown-States für die Suche --- + var showTLDropdown by remember { mutableStateOf(false) } + var showTBDropdown by remember { mutableStateOf(false) } + var showTDDropdown by remember { mutableStateOf(false) } + var showPCDropdown by remember { mutableStateOf(false) } + var showTADropdown by remember { mutableStateOf(false) } + var showSMDropdown by remember { mutableStateOf(false) } + var showSTDropdown by remember { mutableStateOf(false) } + var richter by remember { mutableStateOf( listOf( @@ -68,18 +77,162 @@ fun OrganisationTabContent(viewModel: NennungViewModel) { // ── Funktionäre & Offizielle ───────────────────────────────────────── OrgSectionCard(title = "Funktionäre & Offizielle (C-Satz)") { OrgSubSection("Turnier-Organisation") { - OrgSearchField("Turnierleiter:", turnierleiter) { - turnierleiter = it - // In einem echten Szenario würde hier die Masterdata-Suche getriggert + Box { + OrgSearchField("Turnierleiter:", turnierleiter) { + turnierleiter = it + viewModel.searchFunktionaere(it) + showTLDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showTLDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showTLDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + turnierleiter = f.name + showTLDropdown = false + } + ) + } + } + } + Box { + OrgSearchField("Turnierbeauftragter:", turnierbeauftragter) { + turnierbeauftragter = it + viewModel.searchFunktionaere(it) + showTBDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showTBDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showTBDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + turnierbeauftragter = f.name + showTBDropdown = false + } + ) + } + } + } + Box { + OrgSearchField("Technischer Delegierter:", technischerDelegierter) { + technischerDelegierter = it + viewModel.searchFunktionaere(it) + showTDDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showTDDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showTDDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + technischerDelegierter = f.name + showTDDropdown = false + } + ) + } + } + } + Box { + OrgSearchField("Parcourschef:", parcourschef) { + parcourschef = it + viewModel.searchFunktionaere(it) + showPCDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showPCDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showPCDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + parcourschef = f.name + showPCDropdown = false + } + ) + } + } } - OrgSearchField("Turnierbeauftragter:", turnierbeauftragter) { turnierbeauftragter = it } - OrgSearchField("Technischer Delegierter:", technischerDelegierter) { technischerDelegierter = it } - OrgSearchField("Parcourschef:", parcourschef) { parcourschef = it } } OrgSubSection("Support-Team") { - OrgSearchField("Tierarzt:", tierarzt) { tierarzt = it } - OrgSearchField("Schmied:", schmied) { schmied = it } - OrgSearchField("Steward:", steward) { steward = it } + Box { + OrgSearchField("Tierarzt:", tierarzt) { + tierarzt = it + viewModel.searchFunktionaere(it) + showTADropdown = it.length >= 2 + } + DropdownMenu( + expanded = showTADropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showTADropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + tierarzt = f.name + showTADropdown = false + } + ) + } + } + } + Box { + OrgSearchField("Schmied:", schmied) { + schmied = it + viewModel.searchFunktionaere(it) + showSMDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showSMDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showSMDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + schmied = f.name + showSMDropdown = false + } + ) + } + } + } + Box { + OrgSearchField("Steward:", steward) { + steward = it + viewModel.searchFunktionaere(it) + showSTDropdown = it.length >= 2 + } + DropdownMenu( + expanded = showSTDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showSTDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + steward = f.name + showSTDropdown = false + } + ) + } + } + } } } @@ -109,12 +262,36 @@ fun OrganisationTabContent(viewModel: NennungViewModel) { modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - OutlinedTextField( - value = r.name, - onValueChange = { v -> richter = richter.toMutableList().also { it[idx] = r.copy(name = v) } }, - modifier = Modifier.weight(3f).height(44.dp).padding(end = 8.dp), - singleLine = true, - ) + // Name-Suche mit Dropdown + var showRichterDropdown by remember { mutableStateOf(false) } + Box(modifier = Modifier.weight(3f).padding(end = 8.dp)) { + OutlinedTextField( + value = r.name, + onValueChange = { v -> + richter = richter.toMutableList().also { it[idx] = r.copy(name = v) } + viewModel.searchFunktionaere(v) + showRichterDropdown = v.length >= 2 + }, + modifier = Modifier.fillMaxWidth().height(44.dp), + singleLine = true, + placeholder = { Text("Name suchen...", fontSize = 12.sp) } + ) + DropdownMenu( + expanded = showRichterDropdown && state.searchResultsFunktionaere.isNotEmpty(), + onDismissRequest = { showRichterDropdown = false }, + properties = androidx.compose.ui.window.PopupProperties(focusable = false) + ) { + state.searchResultsFunktionaere.forEach { f -> + DropdownMenuItem( + text = { Text("${f.name} (${f.qualifikationen.joinToString(", ")})") }, + onClick = { + richter = richter.toMutableList().also { it[idx] = r.copy(name = f.name) } + showRichterDropdown = false + } + ) + } + } + } // Qualifikation-Dropdown var qualExpanded by remember { mutableStateOf(false) } Box(modifier = Modifier.weight(1.5f).padding(end = 8.dp)) {