From f82d06f3e77e97f8dba8629730a02c8737d99bd4 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sun, 12 Apr 2026 15:56:06 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 9 ++ ...6-04-12_Masterdata_Editoren_Curator_Log.md | 42 ++++++++ .../feature/domain/MasterdataRepository.kt | 6 ++ .../feature/presentation/NennungViewModel.kt | 50 +++++++++- .../remote/DefaultMasterdataRepository.kt | 48 +++++++++ .../presentation/MasterdataEditDialogs.kt | 97 +++++++++++++++++++ .../presentation/TurnierNennungenTab.kt | 16 +++ .../kotlin/at/mocode/desktop/DesktopApp.kt | 4 + .../jvmMain/kotlin/at/mocode/desktop/main.kt | 2 + .../desktop/screens/preview/ScreenPreviews.kt | 8 ++ 10 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md create mode 100644 frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 06940c28..8be84b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. 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 new file mode 100644 index 00000000..522d523a --- /dev/null +++ b/docs/04_Agents/Logs/2026-04-12_Masterdata_Editoren_Curator_Log.md @@ -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.* diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/MasterdataRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/MasterdataRepository.kt index 4a81b931..aadbc932 100644 --- a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/MasterdataRepository.kt +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/MasterdataRepository.kt @@ -37,7 +37,13 @@ data class Verein( interface MasterdataRepository { suspend fun searchReiter(query: String): Result> + suspend fun getReiter(id: String): Result + suspend fun saveReiter(reiter: Reiter): Result + suspend fun searchPferde(query: String): Result> + suspend fun getPferd(id: String): Result + suspend fun savePferd(pferd: Pferd): Result + suspend fun searchFunktionaere(query: String): Result> suspend fun listVereine(): Result> suspend fun getVereinById(id: String): Result diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt index 7264ff65..b6505f05 100644 --- a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt @@ -14,6 +14,9 @@ data class NennungenState( val nennungen: List = emptyList(), val searchResultsReiter: List = emptyList(), val searchResultsPferde: List = emptyList(), + val searchResultsFunktionaere: List = 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 { diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/data/remote/DefaultMasterdataRepository.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/data/remote/DefaultMasterdataRepository.kt index ea48163d..9e08f571 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/data/remote/DefaultMasterdataRepository.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/data/remote/DefaultMasterdataRepository.kt @@ -22,6 +22,30 @@ class DefaultMasterdataRepository( } else emptyList() } + override suspend fun getReiter(id: String): Result = runCatching { + val response = client.get("${ApiRoutes.Masterdata.REITER}/$id") + if (response.status.isSuccess()) { + response.body().toDomain() + } else throw Exception("Reiter nicht gefunden") + } + + override suspend fun saveReiter(reiter: Reiter): Result = 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().toDomain() + } else throw Exception("Fehler beim Speichern des Reiters") + } + override suspend fun searchPferde(query: String): Result> = 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 = runCatching { + val response = client.get("${ApiRoutes.Masterdata.PFERDE}/$id") + if (response.status.isSuccess()) { + response.body().toDomain() + } else throw Exception("Pferd nicht gefunden") + } + + override suspend fun savePferd(pferd: Pferd): Result = 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().toDomain() + } else throw Exception("Fehler beim Speichern des Pferdes") + } + override suspend fun searchFunktionaere(query: String): Result> = runCatching { val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/search") { parameter("q", query) 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 new file mode 100644 index 00000000..f1661382 --- /dev/null +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/MasterdataEditDialogs.kt @@ -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") + } + } + ) +} diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt index 8e7aad6b..4fc5f8c3 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt @@ -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()) { diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt index 52df7415..85166dbd 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/DesktopApp.kt @@ -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 diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt index e636d265..21095f2b 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/main.kt @@ -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, ) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt index d37e61a5..02af1445 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt @@ -117,6 +117,10 @@ fun PreviewTurnierOrganisationTab() { val mockMasterdataRepo = object : MasterdataRepository { override suspend fun searchReiter(query: String): Result> = Result.success(emptyList()) override suspend fun searchPferde(query: String): Result> = Result.success(emptyList()) + override suspend fun getReiter(id: String): Result = Result.failure(NotImplementedError()) + override suspend fun saveReiter(reiter: Reiter): Result = Result.success(reiter) + override suspend fun getPferd(id: String): Result = Result.failure(NotImplementedError()) + override suspend fun savePferd(pferd: Pferd): Result = Result.success(pferd) override suspend fun searchFunktionaere(query: String): Result> = Result.success(emptyList()) override suspend fun listVereine(): Result> = Result.success(emptyList()) override suspend fun getVereinById(id: String): Result = Result.failure(NotImplementedError()) @@ -185,6 +189,10 @@ fun PreviewTurnierNennungenTab() { val mockMasterdataRepo = object : MasterdataRepository { override suspend fun searchReiter(query: String): Result> = Result.success(emptyList()) override suspend fun searchPferde(query: String): Result> = Result.success(emptyList()) + override suspend fun getReiter(id: String): Result = Result.failure(NotImplementedError()) + override suspend fun saveReiter(reiter: Reiter): Result = Result.success(reiter) + override suspend fun getPferd(id: String): Result = Result.failure(NotImplementedError()) + override suspend fun savePferd(pferd: Pferd): Result = Result.success(pferd) override suspend fun searchFunktionaere(query: String): Result> = Result.success(emptyList()) override suspend fun listVereine(): Result> = Result.success(emptyList()) override suspend fun getVereinById(id: String): Result = Result.failure(NotImplementedError())