diff --git a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md index c61e84a0..62e33aab 100644 --- a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md +++ b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md @@ -61,3 +61,13 @@ Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schr - Erweiterung der Pferde-Suche um die Kopfnummer im `PferdeViewModel`. - Bereinigung von Compiler-Warnungen (unnecessary non-null assertions) in den neuen Screen-Komponenten. - Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls (`BUILD SUCCESSFUL`). + +### 2026-04-21 14:30 - [Lead Architect] & [Frontend Expert] +* **Aktion:** Styling-Vereinheitlichung und Daten-Ownership-Korrektur. +* **Ergebnis:** + * Veranstalter-Verwaltung auf Card-Design umgestellt (MsCard). + * Event-Ownership in den Fake-Daten korrigiert (Neumarkt vs. Linz). + * Globale Event-Verwaltung nach Datum absteigend sortieren. + * MsFilterBar Alignment-Fix (Text-Baseline). + * Reiter-Modelle um Nation und Bundesland erweitert. +* **Status:** Alle fachlichen Anforderungen an das UI-Refining und Datenmodellierung in dieser Session erfüllt. diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsFilterBar.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsFilterBar.kt index 6556c432..743bcc81 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsFilterBar.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsFilterBar.kt @@ -62,7 +62,7 @@ fun MsFilterBar( } } else null, singleLine = true, - textStyle = MaterialTheme.typography.bodySmall, + textStyle = MaterialTheme.typography.bodySmall.copy(baselineShift = androidx.compose.ui.text.style.BaselineShift(0.2f)), shape = MaterialTheme.shapes.small, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), colors = OutlinedTextFieldDefaults.colors( diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterProfilViewModel.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterProfilViewModel.kt index b912dd76..42df6791 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterProfilViewModel.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterProfilViewModel.kt @@ -18,6 +18,8 @@ data class ReiterProfilState( val feiId: String = "", val lizenzKlasse: String = "", val verein: String = "", + val nation: String = "AUT", + val bundesland: String = "", // Validierungsergebnisse (Live-Feedback, ÖTO/FEI Regelwerk) val oepsNummerValidation: ValidationResult = ValidationResult.Ok, val feiIdValidation: ValidationResult = ValidationResult.Ok, @@ -39,6 +41,8 @@ sealed interface ReiterProfilIntent { data class EditFeiId(val v: String) : ReiterProfilIntent data class EditLizenz(val v: String) : ReiterProfilIntent data class EditVerein(val v: String) : ReiterProfilIntent + data class EditNation(val v: String) : ReiterProfilIntent + data class EditBundesland(val v: String) : ReiterProfilIntent data object Save : ReiterProfilIntent data object ClearError : ReiterProfilIntent } @@ -69,6 +73,8 @@ class ReiterProfilViewModel( is ReiterProfilIntent.EditFeiId -> edit { it.copy(feiId = intent.v) } is ReiterProfilIntent.EditLizenz -> edit { it.copy(lizenzKlasse = intent.v) } is ReiterProfilIntent.EditVerein -> edit { it.copy(verein = intent.v) } + is ReiterProfilIntent.EditNation -> edit { it.copy(nation = intent.v) } + is ReiterProfilIntent.EditBundesland -> edit { it.copy(bundesland = intent.v) } is ReiterProfilIntent.Save -> save() is ReiterProfilIntent.ClearError -> reduce { it.copy(errorMessage = null) } } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/data/Stores.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/data/Stores.kt index c4ce79bc..7b1f5fa8 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/data/Stores.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/data/Stores.kt @@ -319,12 +319,14 @@ object Store { datumVon = "2026-05-20", datumBis = "2026-05-24", status = "In Vorbereitung", - beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark." + beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark.", + ort = "Linz-Ebelsberg" ) ) TurnierStore.add( linzId, Turnier(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply { + titel = "Linzer Pferdepage" kategorie.add("CSN-B*") }) @@ -336,7 +338,8 @@ object Store { titel = "Herbst-Turnier 2025", datumVon = "2025-09-15", datumBis = "2025-09-17", - status = "Abgeschlossen" + status = "Abgeschlossen", + ort = "Neumarkt/M." ) ) } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/management/ManagementScreens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/management/ManagementScreens.kt index d052d577..c7ffcf31 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/management/ManagementScreens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/frontend/shell/desktop/screens/management/ManagementScreens.kt @@ -1,21 +1,24 @@ package at.mocode.frontend.shell.desktop.screens.management +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.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import at.mocode.frontend.core.designsystem.components.MsButton +import at.mocode.frontend.core.designsystem.components.MsCard +import at.mocode.frontend.core.designsystem.theme.Dimens import at.mocode.frontend.shell.desktop.data.Store @Composable @@ -254,28 +257,131 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) { @Composable fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) { - // Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten, - // wir nutzen hier die 'vereine' Liste aus dem Store. val vereine = Store.vereine var filter by remember { mutableStateOf("") } val filteredItems = if (filter.isEmpty()) vereine else vereine.filter { it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true) } - ManagementTableScreen( - title = "Veranstalter-Verwaltung", - items = filteredItems, - columns = listOf( - TableColumn("Name", { it.name }, weight = 2f), - TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp), - TableColumn("Ort", { it.ort ?: "-" }, weight = 1f), - TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp), - TableColumn("Email", { it.email ?: "-" }, weight = 1f) - ), - onBack = onBack, - onNew = onNew, - onEdit = { onEdit(it.id) }, - onDelete = { }, - onSearch = { filter = it } - ) + Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Veranstalter - verwalten", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + MsButton( + text = "Neuer Veranstalter", + onClick = onNew + ) + } + + Spacer(Modifier.height(Dimens.SpacingL)) + + OutlinedTextField( + value = filter, + onValueChange = { filter = it }, + placeholder = { Text("Suche nach Name oder ÖPS-Nr...", style = MaterialTheme.typography.bodyMedium) }, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp)) }, + trailingIcon = { + if (filter.isNotEmpty()) { + IconButton(onClick = { filter = "" }) { + Icon(Icons.Default.Clear, contentDescription = "Löschen") + } + } + }, + singleLine = true, + shape = MaterialTheme.shapes.medium, + textStyle = MaterialTheme.typography.bodyMedium + ) + + Spacer(Modifier.height(Dimens.SpacingL)) + + if (filteredItems.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("Keine Veranstalter gefunden.", color = Color.Gray) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM), + contentPadding = PaddingValues(bottom = Dimens.SpacingL) + ) { + items(filteredItems) { veranstalter -> + VeranstalterCard( + name = veranstalter.name, + oepsNr = veranstalter.oepsNummer, + ort = veranstalter.ort ?: "-", + bundesland = veranstalter.bundesland ?: "-", + onClick = { onEdit(veranstalter.id) } + ) + } + } + } + } +} + +@Composable +fun VeranstalterCard( + name: String, + oepsNr: String, + ort: String, + bundesland: String, + onClick: () -> Unit, +) { + MsCard( + modifier = Modifier.fillMaxWidth(), + onClick = onClick + ) { + Row( + modifier = Modifier.padding(Dimens.SpacingM), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Business, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.secondary + ) + } + + Spacer(Modifier.width(Dimens.SpacingM)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS) + ) { + Text( + text = "ÖPS: $oepsNr", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.SemiBold + ) + Text("•", color = Color.Gray) + Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray) + Text("$ort ($bundesland)", style = MaterialTheme.typography.labelSmall, color = Color.Gray) + } + } + + Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray) + } + } }