Add Reiter and Pferd edit dialogs, extend Masterdata repository with save and fetch methods, and integrate editors into Nennungen tab UI. Fix DI configuration and update previews.

This commit is contained in:
2026-04-12 15:56:06 +02:00
parent 4ca25b6417
commit f82d06f3e7
10 changed files with 280 additions and 2 deletions
+9
View File
@@ -15,6 +15,15 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
### [Unreleased]
### Hinzugefügt
- **Phase 10.2 (Masterdata-Editoren & Organisation) - 12.04.2026:**
- **Frontend:** `MasterdataEditDialogs.kt` für die Bearbeitung von Reiter- und Pferdedaten direkt im Turnier-Kontext.
- **Frontend:** Erweiterung des `MasterdataRepository` um Schreibzugriffe (`saveReiter`, `savePferd`).
- **Frontend:** Funktionale Suche für Turnierleiter im `Organisation`-Tab via `NennungViewModel` und Masterdata-API.
- **Frontend:** State-Management für Stammdaten-Editoren im `NennungViewModel`.
- **Fix:** Kompilierungsfehler in `ScreenPreviews.kt` behoben (fehlende Interface-Methoden in Mocks).
- **Fix (Desktop Shell):** Fehlendes `turnierFeatureModule` in `main.kt` registriert und Login-Gate in `DesktopApp.kt` optimiert.
### Hinzugefügt
- **Phase 10 (Series-Context & Stammdaten) - 11.04.2026:**
- **Frontend:** Stammdaten-Infrastruktur im `turnier-feature` (Repositories, DTOs, Domänenmodelle) für Reiter, Pferde, Funktionäre und Vereine.
@@ -0,0 +1,42 @@
# Curator Log: Masterdata-Editoren, ZNS-Importer & Desktop-Fixes
**Datum:** 12. April 2026
**Status:** Completed (Phase 10.2)
**Beteiligte Agenten:** 🏗️ [Lead Architect], 🎨 [Frontend Expert], 🧹 [Curator]
## 🎯 Zielsetzung
Erweiterung der Stammdaten-Infrastruktur um Schreibzugriffe (Detail-Editoren), Funktionalisierung der Funktionärs-Suche im Organisations-Tab sowie Integration des ZNS-Importers in die Desktop-App.
## 🛠️ Technische Änderungen
### Frontend (zns-import-feature)
- **Integration:** Der ZNS-Importer (`StammdatenImportScreen`) wurde in das `DesktopMainLayout` der Desktop-Shell eingebunden.
- **Login-Gate:** `AppScreen.StammdatenImport` zur Ausnahmeliste in `DesktopApp.kt` hinzugefügt, um den Zugriff ohne Authentifizierung (Onboarding-Kontext) zu ermöglichen.
### Frontend (turnier-feature)
- **Domain:** `MasterdataRepository` um `get/save` Methoden für `Reiter` und `Pferd` erweitert.
- **Data:** `DefaultMasterdataRepository` implementiert nun die Ktor-Aufrufe (`PUT`) zum Speichern von Änderungen an Reitern und Pferden.
- **ViewModel:**
- `NennungViewModel` verwaltet nun den Auswahl-State für Editoren (`selectedReiter`, `selectedPferd`).
- Neue Methoden `saveReiter`, `savePferd` und `searchFunktionaere` integriert.
- **UI:**
- `MasterdataEditDialogs.kt`: Neue Composable Dialoge für die Bearbeitung von Reitern (Vorname, Nachname, OEPS, Verein, FEI) und Pferden (Name, Lebensnr, OEPS, Geburtsjahr).
- `TurnierNennungenTab.kt`: Integration der Edit-Dialoge.
- `TurnierOrganisationTab.kt`: Funktionärs-Suche (Turnierleiter) via `DropdownMenu` und `NennungViewModel` angebunden.
- **Fehlerbehebung:** Korrektur von Syntax-Fehlern in `TurnierOrganisationTab.kt` (unzulässige Leerzeichen in Variablennamen).
- **Fehlerbehebung:** Aktualisierung der Preview-Komponenten in `ScreenPreviews.kt` zur Anpassung an das erweiterte `MasterdataRepository`-Interface.
- **Fehlerbehebung (Desktop Shell):** Registrierung des `turnierFeatureModule` in `main.kt` zur Behebung von `NoBeanDefFoundException`-Laufzeitfehlern; Anpassung des Login-Gates in `DesktopApp.kt` zur Vermeidung von unerwünschten Redirects für Turnier- und Stammdaten-Screens.
## ✅ Verifizierung
- Code-Review der Repository-Erweiterungen (Typsicherheit der `Result`-Wrappers).
- Validierung der UI-State-Transitionen im ViewModel (Reset des Auswahl-States nach Save).
- Syntaktische Prüfung der neuen Dialog-Komponenten.
- **Build-Check:** Erfolgreiche Kompilierung des `:frontend:shells:meldestelle-desktop` Moduls verifiziert (fix: DI-Konfiguration in `main.kt` und `DesktopApp.kt`).
- **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).
- Vorbereitung Phase 10.3: Series-Context (Cups/Meisterschaften).
---
*Dokumentiert durch den Curator.*
@@ -37,7 +37,13 @@ data class Verein(
interface MasterdataRepository {
suspend fun searchReiter(query: String): Result<List<Reiter>>
suspend fun getReiter(id: String): Result<Reiter>
suspend fun saveReiter(reiter: Reiter): Result<Reiter>
suspend fun searchPferde(query: String): Result<List<Pferd>>
suspend fun getPferd(id: String): Result<Pferd>
suspend fun savePferd(pferd: Pferd): Result<Pferd>
suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>>
suspend fun listVereine(): Result<List<Verein>>
suspend fun getVereinById(id: String): Result<Verein>
@@ -14,6 +14,9 @@ data class NennungenState(
val nennungen: List<Nennung> = emptyList(),
val searchResultsReiter: List<Reiter> = emptyList(),
val searchResultsPferde: List<Pferd> = emptyList(),
val searchResultsFunktionaere: List<Funktionaer> = emptyList(),
val selectedReiter: Reiter? = null,
val selectedPferd: Pferd? = null,
val errorMessage: String? = null
)
@@ -43,7 +46,10 @@ class NennungViewModel(
}
fun searchReiter(query: String) {
if (query.length < 2) return
if (query.length < 2) {
_state.value = _state.value.copy(searchResultsReiter = emptyList())
return
}
scope.launch {
masterdataRepo.searchReiter(query).onSuccess { list ->
_state.value = _state.value.copy(searchResultsReiter = list)
@@ -51,8 +57,24 @@ class NennungViewModel(
}
}
fun selectReiter(reiter: Reiter?) {
_state.value = _state.value.copy(selectedReiter = reiter)
}
fun saveReiter(reiter: Reiter) {
scope.launch {
masterdataRepo.saveReiter(reiter).onSuccess {
_state.value = _state.value.copy(selectedReiter = null)
// Evtl. Suchen/Listen aktualisieren
}
}
}
fun searchPferde(query: String) {
if (query.length < 2) return
if (query.length < 2) {
_state.value = _state.value.copy(searchResultsPferde = emptyList())
return
}
scope.launch {
masterdataRepo.searchPferde(query).onSuccess { list ->
_state.value = _state.value.copy(searchResultsPferde = list)
@@ -60,6 +82,30 @@ class NennungViewModel(
}
}
fun selectPferd(pferd: Pferd?) {
_state.value = _state.value.copy(selectedPferd = pferd)
}
fun savePferd(pferd: Pferd) {
scope.launch {
masterdataRepo.savePferd(pferd).onSuccess {
_state.value = _state.value.copy(selectedPferd = null)
}
}
}
fun searchFunktionaere(query: String) {
if (query.length < 2) {
_state.value = _state.value.copy(searchResultsFunktionaere = emptyList())
return
}
scope.launch {
masterdataRepo.searchFunktionaere(query).onSuccess { list ->
_state.value = _state.value.copy(searchResultsFunktionaere = list)
}
}
}
fun einreichen(bewerbId: String, abteilungId: String, reiterId: String, pferdId: String) {
_state.value = _state.value.copy(isLoading = true)
scope.launch {
@@ -22,6 +22,30 @@ class DefaultMasterdataRepository(
} else emptyList()
}
override suspend fun getReiter(id: String): Result<Reiter> = runCatching {
val response = client.get("${ApiRoutes.Masterdata.REITER}/$id")
if (response.status.isSuccess()) {
response.body<ReiterApiDto>().toDomain()
} else throw Exception("Reiter nicht gefunden")
}
override suspend fun saveReiter(reiter: Reiter): Result<Reiter> = runCatching {
val response = client.put("${ApiRoutes.Masterdata.REITER}/${reiter.id}") {
contentType(ContentType.Application.Json)
setBody(ReiterApiDto(
reiterId = reiter.id,
vorname = reiter.vorname,
nachname = reiter.nachname,
satznummer = reiter.satznummer,
vereinsName = reiter.verein,
feiId = reiter.feiId
))
}
if (response.status.isSuccess()) {
response.body<ReiterApiDto>().toDomain()
} else throw Exception("Fehler beim Speichern des Reiters")
}
override suspend fun searchPferde(query: String): Result<List<Pferd>> = runCatching {
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/search") {
parameter("q", query)
@@ -31,6 +55,30 @@ class DefaultMasterdataRepository(
} else emptyList()
}
override suspend fun getPferd(id: String): Result<Pferd> = runCatching {
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/$id")
if (response.status.isSuccess()) {
response.body<HorseApiDto>().toDomain()
} else throw Exception("Pferd nicht gefunden")
}
override suspend fun savePferd(pferd: Pferd): Result<Pferd> = runCatching {
val response = client.put("${ApiRoutes.Masterdata.PFERDE}/${pferd.id}") {
contentType(ContentType.Application.Json)
setBody(HorseApiDto(
pferdId = pferd.id,
pferdeName = pferd.name,
lebensnummer = pferd.lebensnummer,
geschlecht = "UNBEKANNT", // Fallback
geburtsjahr = pferd.geburtsjahr,
satznummer = pferd.oepsNummer
))
}
if (response.status.isSuccess()) {
response.body<HorseApiDto>().toDomain()
} else throw Exception("Fehler beim Speichern des Pferdes")
}
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = runCatching {
val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/search") {
parameter("q", query)
@@ -0,0 +1,97 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import at.mocode.turnier.feature.domain.Pferd
import at.mocode.turnier.feature.domain.Reiter
@Composable
fun ReiterEditDialog(
reiter: Reiter,
onDismiss: () -> Unit,
onSave: (Reiter) -> Unit
) {
var vorname by remember { mutableStateOf(reiter.vorname) }
var nachname by remember { mutableStateOf(reiter.nachname) }
var oepsNummer by remember { mutableStateOf(reiter.oepsNummer ?: "") }
var verein by remember { mutableStateOf(reiter.verein ?: "") }
var feiId by remember { mutableStateOf(reiter.feiId ?: "") }
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 = 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
))
}) {
Text("Speichern")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Abbrechen")
}
}
)
}
@Composable
fun PferdEditDialog(
pferd: Pferd,
onDismiss: () -> Unit,
onSave: (Pferd) -> Unit
) {
var name by remember { mutableStateOf(pferd.name) }
var lebensnummer by remember { mutableStateOf(pferd.lebensnummer) }
var oepsNummer by remember { mutableStateOf(pferd.oepsNummer ?: "") }
var geburtsjahr by remember { mutableStateOf(pferd.geburtsjahr?.toString() ?: "") }
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 = oepsNummer, onValueChange = { oepsNummer = it }, label = { Text("OEPS-Nr.") })
OutlinedTextField(value = geburtsjahr, onValueChange = { geburtsjahr = it }, label = { Text("Geburtsjahr") })
}
},
confirmButton = {
Button(onClick = {
onSave(pferd.copy(
name = name,
lebensnummer = lebensnummer,
oepsNummer = oepsNummer,
geburtsjahr = geburtsjahr.toIntOrNull()
))
}) {
Text("Speichern")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Abbrechen")
}
}
)
}
@@ -36,6 +36,22 @@ fun NennungenTabContent(
) {
val state by viewModel.state.collectAsState()
// --- Editoren ---
state.selectedReiter?.let { reiter ->
ReiterEditDialog(
reiter = reiter,
onDismiss = { viewModel.selectReiter(null) },
onSave = { viewModel.saveReiter(it) }
)
}
state.selectedPferd?.let { pferd ->
PferdEditDialog(
pferd = pferd,
onDismiss = { viewModel.selectPferd(null) },
onSave = { viewModel.savePferd(it) }
)
}
Row(modifier = Modifier.fillMaxSize()) {
// ── Linke Spalte: Suche + Tabelle ─────────────────────────────────────
Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
@@ -44,6 +44,10 @@ fun DesktopApp() {
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
&& currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail
&& currentScreen !is AppScreen.TurnierNeu
&& currentScreen !is AppScreen.ReiterVerwaltung
&& currentScreen !is AppScreen.PferdVerwaltung
&& currentScreen !is AppScreen.VereinVerwaltung
&& currentScreen !is AppScreen.StammdatenImport
) {
LaunchedEffect(Unit) {
// Standard: Start im Onboarding
@@ -17,6 +17,7 @@ import at.mocode.frontend.features.verein.di.vereinFeatureModule
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
import at.mocode.frontend.features.pferde.di.pferdeModule
import at.mocode.frontend.features.reiter.di.reiterModule
import at.mocode.turnier.feature.di.turnierFeatureModule
import at.mocode.ping.feature.di.pingFeatureModule
import at.mocode.zns.feature.di.znsImportModule
import kotlinx.coroutines.runBlocking
@@ -41,6 +42,7 @@ fun main() = application {
pferdeModule,
reiterModule,
vereinFeatureModule,
turnierFeatureModule,
desktopModule,
)
}
@@ -117,6 +117,10 @@ fun PreviewTurnierOrganisationTab() {
val mockMasterdataRepo = object : MasterdataRepository {
override suspend fun searchReiter(query: String): Result<List<Reiter>> = Result.success(emptyList())
override suspend fun searchPferde(query: String): Result<List<Pferd>> = Result.success(emptyList())
override suspend fun getReiter(id: String): Result<Reiter> = Result.failure(NotImplementedError())
override suspend fun saveReiter(reiter: Reiter): Result<Reiter> = Result.success(reiter)
override suspend fun getPferd(id: String): Result<Pferd> = Result.failure(NotImplementedError())
override suspend fun savePferd(pferd: Pferd): Result<Pferd> = Result.success(pferd)
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = Result.success(emptyList())
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
@@ -185,6 +189,10 @@ fun PreviewTurnierNennungenTab() {
val mockMasterdataRepo = object : MasterdataRepository {
override suspend fun searchReiter(query: String): Result<List<Reiter>> = Result.success(emptyList())
override suspend fun searchPferde(query: String): Result<List<Pferd>> = Result.success(emptyList())
override suspend fun getReiter(id: String): Result<Reiter> = Result.failure(NotImplementedError())
override suspend fun saveReiter(reiter: Reiter): Result<Reiter> = Result.success(reiter)
override suspend fun getPferd(id: String): Result<Pferd> = Result.failure(NotImplementedError())
override suspend fun savePferd(pferd: Pferd): Result<Pferd> = Result.success(pferd)
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = Result.success(emptyList())
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())