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 fdab9c97..c61e84a0 100644 --- a/docs/99_Journal/2026-04-21_Curator_Session_Summary.md +++ b/docs/99_Journal/2026-04-21_Curator_Session_Summary.md @@ -47,3 +47,17 @@ Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schr - Korrektur der `LaunchedEffect`-Logik in `DesktopMainLayout.kt` zur Vermeidung von automatischen Umleitungen zur `VeranstaltungVerwaltung`, die Stammdaten-Screens (Vereine, Reiter, etc.) blockierten. - Erweiterung des Login-Gates in `DesktopApp.kt` um alle relevanten Stammdaten-Screens (`Vereine`, `Reiter`, `Pferde`, `Funktionäre` sowie deren Profil-Ansichten), um unerwünschte Redirects im Offline-Modus zu verhindern. - Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls. + +### 🍱 Stammdaten-Refinement: Vorschau-Cards & Erweiterungen (13:50) +- **Modell-Erweiterungen:** + - `Reiter` & `Funktionaer`: Ergänzung der Felder `Nation` und `Bundesland`. + - `Pferd`: Einführung der `Kopfnummer`. + - `Funktionaer`: Konsolidierung der `Qualifikation`. +- **UI-Modernisierung:** + - Implementierung von `ReiterCardPreview`, `PferdCardPreview` und `FunktionaerCardPreview` zur schnellen Identifikation in der Detailansicht. + - Integration dieser Vorschau-Cards in die jeweiligen Screens (`Master-Detail-Layout`) sowie als Orientierungshilfe in den Editoren. + - Anpassung der Pferde-Liste um die Spalte `Kopf-Nr.`. +- **Suche & Performance:** + - 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`). diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/domain/Funktionaer.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/domain/Funktionaer.kt index 060a0803..2d06bc73 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/domain/Funktionaer.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/domain/Funktionaer.kt @@ -11,5 +11,8 @@ data class Funktionaer( val email: String? = null, val telefon: String? = null, val vereinsNummer: String? = null, + val nation: String? = "AUT", + val bundesland: String? = null, + val qualifikation: String? = null, val istAktiv: Boolean = true ) diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt index 4630cf97..0eab9f83 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerScreen.kt @@ -1,15 +1,19 @@ package at.mocode.frontend.features.funktionaer.presentation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Gavel +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue 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.* @@ -40,14 +44,21 @@ fun FunktionaerScreen( onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) }, onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) }, onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) }, + onNationChange = { viewModel.send(FunktionaerIntent.EditNation(it)) }, + onBundeslandChange = { viewModel.send(FunktionaerIntent.EditBundesland(it)) }, + onQualifikationChange = { viewModel.send(FunktionaerIntent.EditQualifikation(it)) }, onSave = { viewModel.send(FunktionaerIntent.Save) }, onCancel = { viewModel.send(FunktionaerIntent.Cancel) } ) } else if (state.selectedFunktionaer != null) { - FunktionaerCard( - funktionaer = state.selectedFunktionaer!!, - onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) } - ) + Column(Modifier.fillMaxSize()) { + FunktionaerCardPreview(funktionaer = state.selectedFunktionaer!!) + Spacer(Modifier.height(16.dp)) + FunktionaerCard( + funktionaer = state.selectedFunktionaer!!, + onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) } + ) + } } else { PlaceholderContent( title = "Kein Funktionär ausgewählt", @@ -123,61 +134,26 @@ fun FunktionaerCard( horizontalAlignment = Alignment.CenterHorizontally ) { Card( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ) + modifier = Modifier.fillMaxWidth().wrapContentHeight() ) { Column(modifier = Modifier.padding(24.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Surface( - modifier = Modifier.size(48.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - Icons.Default.Gavel, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } - } - Spacer(Modifier.width(16.dp)) - Column { - Text( - "${funktionaer.vorname} ${funktionaer.nachname}", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - Text( - "Richter-Nr: ${funktionaer.richterNummer ?: "-"}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - MsStatusBadge( - text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv", - containerColor = (if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error).copy(alpha = 0.1f), - contentColor = if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) + Row(modifier = Modifier.fillMaxWidth()) { + FunktionaerDetailItem(label = "Richter-Nr.", value = funktionaer.richterNummer ?: "-", modifier = Modifier.weight(1f)) + FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f)) } - Spacer(Modifier.height(24.dp)) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) Row(modifier = Modifier.fillMaxWidth()) { - FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f)) - FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f)) + FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.qualifikation ?: "-", modifier = Modifier.weight(1f)) + FunktionaerDetailItem(label = "Sparte(n)", value = funktionaer.qualifiziertFuerSparten.joinToString(", ").ifBlank { "-" }, modifier = Modifier.weight(1f)) + } + + Spacer(Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + FunktionaerDetailItem(label = "Nation", value = funktionaer.nation ?: "-", modifier = Modifier.weight(1f)) + FunktionaerDetailItem(label = "Bundesland", value = funktionaer.bundesland ?: "-", modifier = Modifier.weight(1f)) } Spacer(Modifier.height(16.dp)) @@ -199,6 +175,50 @@ fun FunktionaerCard( } } +@Composable +fun FunktionaerCardPreview(funktionaer: Funktionaer) { + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary) + } + + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text("${funktionaer.vorname} ${funktionaer.nachname}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + MsStatusBadge( + text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv", + containerColor = (if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray).copy(alpha = 0.1f), + contentColor = if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray + ) + } + Text( + text = buildString { + append("Nr: ${funktionaer.richterNummer ?: "-"}") + if (!funktionaer.nation.isNullOrBlank()) append(" | ${funktionaer.nation}") + if (!funktionaer.bundesland.isNullOrBlank()) append(" (${funktionaer.bundesland})") + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + @Composable private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) { Column(modifier = modifier) { @@ -215,17 +235,26 @@ private fun FunktionaerEditorContent( onRichterNummerChange: (String) -> Unit, onEmailChange: (String) -> Unit, onTelefonChange: (String) -> Unit, + onNationChange: (String) -> Unit, + onBundeslandChange: (String) -> Unit, + onQualifikationChange: (String) -> Unit, onSave: () -> Unit, onCancel: () -> Unit ) { - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { MsActionToolbar( - title = "Funktionär Details", + title = if (state.selectedFunktionaer == null) "Funktionär anlegen" else "Funktionär Details", onSave = onSave, onCancel = onCancel ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) + + // Preview in Editor + if (state.selectedFunktionaer != null) { + FunktionaerCardPreview(funktionaer = state.selectedFunktionaer) + Spacer(Modifier.height(16.dp)) + } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsTextField( @@ -246,13 +275,41 @@ private fun FunktionaerEditorContent( Spacer(Modifier.height(16.dp)) - MsTextField( - value = state.editRichterNummer, - onValueChange = onRichterNummerChange, - label = "Richter-Nummer", - modifier = Modifier.width(300.dp), - compact = true - ) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + MsTextField( + value = state.editRichterNummer, + onValueChange = onRichterNummerChange, + label = "Richter-Nummer", + modifier = Modifier.weight(1f), + compact = true + ) + MsTextField( + value = state.editQualifikation, + onValueChange = onQualifikationChange, + label = "Qualifikation", + modifier = Modifier.weight(1f), + compact = true + ) + } + + Spacer(Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + MsTextField( + value = state.editNation, + onValueChange = onNationChange, + label = "Nation", + modifier = Modifier.weight(1f), + compact = true + ) + MsTextField( + value = state.editBundesland, + onValueChange = onBundeslandChange, + label = "Bundesland", + modifier = Modifier.weight(1f), + compact = true + ) + } Spacer(Modifier.height(16.dp)) diff --git a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt index 84dfb565..a1c72f4a 100644 --- a/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt +++ b/frontend/features/funktionaer-feature/src/commonMain/kotlin/at/mocode/frontend/features/funktionaer/presentation/FunktionaerViewModel.kt @@ -1,8 +1,8 @@ package at.mocode.frontend.features.funktionaer.presentation -import at.mocode.frontend.features.funktionaer.domain.Funktionaer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.mocode.frontend.features.funktionaer.domain.Funktionaer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -26,6 +26,9 @@ data class FunktionaerState( val editRichterNummer: String = "", val editEmail: String = "", val editTelefon: String = "", + val editNation: String = "AUT", + val editBundesland: String = "", + val editQualifikation: String = "", val errorMessage: String? = null, ) @@ -40,6 +43,9 @@ sealed interface FunktionaerIntent { data class EditRichterNummer(val value: String) : FunktionaerIntent data class EditEmail(val value: String) : FunktionaerIntent data class EditTelefon(val value: String) : FunktionaerIntent + data class EditNation(val value: String) : FunktionaerIntent + data class EditBundesland(val value: String) : FunktionaerIntent + data class EditQualifikation(val value: String) : FunktionaerIntent data object Save : FunktionaerIntent data object Cancel : FunktionaerIntent data object ClearError : FunktionaerIntent @@ -69,7 +75,10 @@ class FunktionaerViewModel( editNachname = intent.funktionaer?.nachname ?: "", editRichterNummer = intent.funktionaer?.richterNummer ?: "", editEmail = intent.funktionaer?.email ?: "", - editTelefon = intent.funktionaer?.telefon ?: "" + editTelefon = intent.funktionaer?.telefon ?: "", + editNation = intent.funktionaer?.nation ?: "AUT", + editBundesland = intent.funktionaer?.bundesland ?: "", + editQualifikation = intent.funktionaer?.qualifikation ?: "" ) } @@ -81,7 +90,10 @@ class FunktionaerViewModel( editNachname = "", editRichterNummer = "", editEmail = "", - editTelefon = "" + editTelefon = "", + editNation = "AUT", + editBundesland = "", + editQualifikation = "" ) } @@ -90,6 +102,9 @@ class FunktionaerViewModel( is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) } is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) } is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) } + is FunktionaerIntent.EditNation -> reduce { it.copy(editNation = intent.value) } + is FunktionaerIntent.EditBundesland -> reduce { it.copy(editBundesland = intent.value) } + is FunktionaerIntent.EditQualifikation -> reduce { it.copy(editQualifikation = intent.value) } is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) } is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) } is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) } diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt index 14088d6f..f9d1b27b 100644 --- a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/domain/Pferd.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color data class Pferd( val id: String, val name: String, + val kopfNummer: String? = null, val lebensnummer: String, val geschlecht: Geschlecht = Geschlecht.WALLACH, val farbe: String = "", diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt index ad042273..dcb92dd2 100644 --- a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeScreen.kt @@ -1,6 +1,8 @@ package at.mocode.frontend.features.pferde.presentation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Pets @@ -8,6 +10,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import at.mocode.frontend.core.designsystem.components.* @@ -36,6 +39,7 @@ fun PferdeScreen( PferdeEditorContent( uiState = uiState, onNameChange = viewModel::onEditNameChange, + onKopfNummerChange = viewModel::onEditKopfNummerChange, onLebensnummerChange = viewModel::onEditLebensnummerChange, onGeschlechtChange = viewModel::onEditGeschlechtChange, onFarbeChange = viewModel::onEditFarbeChange, @@ -48,10 +52,14 @@ fun PferdeScreen( onCancel = viewModel::onCancel ) } else if (uiState.selectedPferd != null) { - PferdCard( - pferd = uiState.selectedPferd, - onEdit = { viewModel.selectPferd(uiState.selectedPferd) } - ) + Column(Modifier.fillMaxSize()) { + PferdCardPreview(pferd = uiState.selectedPferd) + Spacer(Modifier.height(16.dp)) + PferdCard( + pferd = uiState.selectedPferd, + onEdit = { viewModel.selectPferd(uiState.selectedPferd) } + ) + } } else { PlaceholderContent( title = "Kein Pferd ausgewählt", @@ -88,6 +96,11 @@ private fun PferdeListContent( MsDataTable( items = uiState.searchResults, columns = listOf( + MsColumnDefinition( + title = "Kopf-Nr.", + width = 80.dp, + cellRenderer = { Text(it.kopfNummer ?: "-", style = MaterialTheme.typography.bodySmall) } + ), MsColumnDefinition( title = "Name", weight = 1f, @@ -127,61 +140,12 @@ fun PferdCard( horizontalAlignment = Alignment.CenterHorizontally ) { Card( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ) + modifier = Modifier.fillMaxWidth().wrapContentHeight() ) { Column(modifier = Modifier.padding(24.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Surface( - modifier = Modifier.size(48.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - Icons.Default.Pets, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } - } - Spacer(Modifier.width(16.dp)) - Column { - Text( - pferd.name, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - Text( - pferd.lebensnummer, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - MsStatusBadge( - text = pferd.status.label, - containerColor = pferd.status.color.copy(alpha = 0.1f), - contentColor = pferd.status.color - ) - } - - Spacer(Modifier.height(24.dp)) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - Spacer(Modifier.height(24.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f)) - DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f)) + DetailItem(label = "Kopf-Nummer", value = pferd.kopfNummer ?: "-", modifier = Modifier.weight(1f)) + DetailItem(label = "Lebensnummer", value = pferd.lebensnummer, modifier = Modifier.weight(1f)) } Spacer(Modifier.height(16.dp)) @@ -195,15 +159,62 @@ fun PferdCard( Row(modifier = Modifier.fillMaxWidth()) { DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f)) + DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f)) + } + + Spacer(Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f)) DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f)) } Spacer(Modifier.height(32.dp)) MsButton( - onClick = onEdit, text = "Pferdedaten bearbeiten", - modifier = Modifier.fillMaxWidth() + onClick = onEdit, + fullWidth = true + ) + } + } + } +} + +@Composable +fun PferdCardPreview(pferd: Pferd) { + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.Pets, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary) + } + + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text(pferd.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + MsStatusBadge( + text = pferd.status.label, + containerColor = pferd.status.color.copy(alpha = 0.1f), + contentColor = pferd.status.color + ) + } + Text( + text = "Kopf-Nr: ${pferd.kopfNummer ?: "-"} | Lebensnummer: ${pferd.lebensnummer}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -222,6 +233,7 @@ private fun DetailItem(label: String, value: String, modifier: Modifier = Modifi private fun PferdeEditorContent( uiState: PferdeUiState, onNameChange: (String) -> Unit, + onKopfNummerChange: (String) -> Unit, onLebensnummerChange: (String) -> Unit, onGeschlechtChange: (Geschlecht) -> Unit, onFarbeChange: (String) -> Unit, @@ -233,23 +245,41 @@ private fun PferdeEditorContent( onSave: () -> Unit, onCancel: () -> Unit ) { - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { MsActionToolbar( - title = "Pferde Details", + title = if (uiState.selectedPferd == null) "Pferd anlegen" else "Pferde Details", onSave = onSave, onCancel = onCancel ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) + + // Preview in Editor + if (uiState.selectedPferd != null) { + PferdCardPreview(pferd = uiState.selectedPferd) + Spacer(Modifier.height(16.dp)) + } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsTextField( value = uiState.editName, onValueChange = onNameChange, - label = "Name", + label = "Pferdename", + modifier = Modifier.weight(1.5f), + compact = true + ) + MsTextField( + value = uiState.editKopfNummer, + onValueChange = onKopfNummerChange, + label = "Kopf-Nummer", modifier = Modifier.weight(1f), compact = true ) + } + + Spacer(Modifier.height(16.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsTextField( value = uiState.editLebensnummer, onValueChange = onLebensnummerChange, @@ -257,6 +287,13 @@ private fun PferdeEditorContent( modifier = Modifier.weight(1f), compact = true ) + MsTextField( + value = uiState.editOepsNummer, + onValueChange = onOepsNummerChange, + label = "ÖPS Nummer", + modifier = Modifier.weight(1f), + compact = true + ) } Spacer(Modifier.height(16.dp)) diff --git a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt index fa8703a1..fe7760a1 100644 --- a/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt +++ b/frontend/features/pferde-feature/src/commonMain/kotlin/at/mocode/frontend/features/pferde/presentation/PferdeViewModel.kt @@ -26,7 +26,8 @@ data class PferdeUiState( val editStatus: PferdeStatus = PferdeStatus.AKTIV, val editFeiId: String = "", val editOepsNummer: String = "", - val editBesitzer: String = "" + val editBesitzer: String = "", + val editKopfNummer: String = "" ) /** @@ -44,16 +45,33 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() { private fun loadPferde() { val mockData = listOf( - Pferd("1", "Bella", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV), - Pferd("2", "Casanova", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV), - Pferd("3", "Spirit", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV), - Pferd("4", "Lucky", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT) + Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV), + Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV), + Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV), + Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT) ) uiState = uiState.copy(searchResults = mockData) } fun onSearchQueryChange(query: String) { uiState = uiState.copy(searchQuery = query) + val allPferde = listOf( + Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV), + Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV), + Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV), + Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT) + ) + + val filtered = if (query.isBlank()) { + allPferde + } else { + allPferde.filter { + it.name.contains(query, ignoreCase = true) || + it.lebensnummer.contains(query, ignoreCase = true) || + (it.kopfNummer?.contains(query, ignoreCase = true) ?: false) + } + } + uiState = uiState.copy(searchResults = filtered) } fun selectPferd(pferd: Pferd) { @@ -69,7 +87,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() { editStatus = pferd.status, editFeiId = pferd.feiId ?: "", editOepsNummer = pferd.oepsNummer ?: "", - editBesitzer = pferd.besitzer ?: "" + editBesitzer = pferd.besitzer ?: "", + editKopfNummer = pferd.kopfNummer ?: "" ) } @@ -86,7 +105,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() { editStatus = PferdeStatus.AKTIV, editFeiId = "", editOepsNummer = "", - editBesitzer = "" + editBesitzer = "", + editKopfNummer = "" ) } @@ -102,6 +122,10 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() { uiState = uiState.copy(editBesitzer = value) } + fun onEditKopfNummerChange(value: String) { + uiState = uiState.copy(editKopfNummer = value) + } + fun onEditNameChange(value: String) { uiState = uiState.copy(editName = value) } diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt index 4ca98d8a..3a69c557 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/domain/Reiter.kt @@ -18,7 +18,9 @@ data class Reiter( val geburtsdatum: String? = null, val email: String? = null, val telefon: String? = null, - val verein: String? = null + val verein: String? = null, + val nation: String? = "AUT", + val bundesland: String? = null ) { val name: String get() = "$vorname $nachname" } diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt index 18c0d7c7..87d06024 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterScreen.kt @@ -1,10 +1,16 @@ package at.mocode.frontend.features.reiter.presentation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import at.mocode.frontend.core.designsystem.components.* import at.mocode.frontend.core.designsystem.models.PlaceholderContent @@ -41,14 +47,20 @@ fun ReiterScreen( onEmailChange = viewModel::onEditEmailChange, onTelefonChange = viewModel::onEditTelefonChange, onVereinChange = viewModel::onEditVereinChange, + onNationChange = viewModel::onEditNationChange, + onBundeslandChange = viewModel::onEditBundeslandChange, onSave = viewModel::onSave, onCancel = viewModel::onCancel ) } else if (uiState.selectedReiter != null) { - ReiterCard( - reiter = uiState.selectedReiter, - onEdit = { viewModel.selectReiter(uiState.selectedReiter) } - ) + Column(Modifier.fillMaxSize()) { + ReiterCardPreview(reiter = uiState.selectedReiter) + Spacer(Modifier.height(16.dp)) + ReiterCard( + reiter = uiState.selectedReiter, + onEdit = { viewModel.selectReiter(uiState.selectedReiter) } + ) + } } else { PlaceholderContent( title = "Kein Reiter ausgewählt", @@ -122,57 +134,9 @@ fun ReiterCard( horizontalAlignment = Alignment.CenterHorizontally ) { Card( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ) + modifier = Modifier.fillMaxWidth().wrapContentHeight() ) { Column(modifier = Modifier.padding(24.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Surface( - modifier = Modifier.size(48.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = (reiter.vorname.take(1) + reiter.nachname.take(1)).uppercase(), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - } - } - Spacer(Modifier.width(16.dp)) - Column { - Text( - reiter.name, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - "ÖPS-Nr: ${reiter.oepsNummer ?: "-"}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - MsStatusBadge( - text = reiter.status.label, - containerColor = reiter.status.color.copy(alpha = 0.1f), - contentColor = reiter.status.color - ) - } - - Spacer(Modifier.height(24.dp)) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - Spacer(Modifier.height(24.dp)) - Row(modifier = Modifier.fillMaxWidth()) { ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f)) ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f)) @@ -192,6 +156,13 @@ fun ReiterCard( ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f)) } + Spacer(Modifier.height(16.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + ReiterDetailItem(label = "Nation", value = reiter.nation ?: "-", modifier = Modifier.weight(1f)) + ReiterDetailItem(label = "Bundesland", value = reiter.bundesland ?: "-", modifier = Modifier.weight(1f)) + } + Spacer(Modifier.height(32.dp)) MsButton( @@ -204,6 +175,50 @@ fun ReiterCard( } } +@Composable +fun ReiterCardPreview(reiter: Reiter) { + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary) + } + + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text(reiter.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + MsStatusBadge( + text = reiter.status.label, + containerColor = reiter.status.color.copy(alpha = 0.1f), + contentColor = reiter.status.color + ) + } + Text( + text = buildString { + append("ÖPS: ${reiter.oepsNummer ?: "-"}") + if (!reiter.nation.isNullOrBlank()) append(" | ${reiter.nation}") + if (!reiter.bundesland.isNullOrBlank()) append(" (${reiter.bundesland})") + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + @Composable private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) { Column(modifier = modifier) { @@ -225,17 +240,25 @@ private fun ReiterEditorContent( onEmailChange: (String) -> Unit, onTelefonChange: (String) -> Unit, onVereinChange: (String) -> Unit, + onNationChange: (String) -> Unit, + onBundeslandChange: (String) -> Unit, onSave: () -> Unit, onCancel: () -> Unit ) { - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { MsActionToolbar( - title = "Reiter Details", + title = if (uiState.selectedReiter == null) "Reiter anlegen" else "Reiter Details", onSave = onSave, onCancel = onCancel ) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) + + // Preview in Editor + if (uiState.selectedReiter != null) { + ReiterCardPreview(reiter = uiState.selectedReiter) + Spacer(Modifier.height(16.dp)) + } Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsTextField( @@ -294,6 +317,25 @@ private fun ReiterEditorContent( Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + MsTextField( + value = uiState.editNation, + onValueChange = onNationChange, + label = "Nation", + modifier = Modifier.weight(1f), + compact = true + ) + MsTextField( + value = uiState.editBundesland, + onValueChange = onBundeslandChange, + label = "Bundesland", + modifier = Modifier.weight(1f), + compact = true + ) + } + + Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { MsTextField( value = uiState.editEmail, diff --git a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt index 11003d86..5d78bfe3 100644 --- a/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt +++ b/frontend/features/reiter-feature/src/commonMain/kotlin/at/mocode/frontend/features/reiter/presentation/ReiterViewModel.kt @@ -29,7 +29,9 @@ data class ReiterUiState( val editGeburtsdatum: String = "", val editEmail: String = "", val editTelefon: String = "", - val editVerein: String = "" + val editVerein: String = "", + val editNation: String = "AUT", + val editBundesland: String = "" ) /** @@ -77,7 +79,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() { editGeburtsdatum = reiter.geburtsdatum ?: "", editEmail = reiter.email ?: "", editTelefon = reiter.telefon ?: "", - editVerein = reiter.verein ?: "" + editVerein = reiter.verein ?: "", + editNation = reiter.nation ?: "AUT", + editBundesland = reiter.bundesland ?: "" ) } @@ -96,7 +100,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() { editGeburtsdatum = "", editEmail = "", editTelefon = "", - editVerein = "" + editVerein = "", + editNation = "AUT", + editBundesland = "" ) } @@ -106,6 +112,8 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() { fun onEditEmailChange(value: String) { uiState = uiState.copy(editEmail = value) } fun onEditTelefonChange(value: String) { uiState = uiState.copy(editTelefon = value) } fun onEditVereinChange(value: String) { uiState = uiState.copy(editVerein = value) } + fun onEditNationChange(value: String) { uiState = uiState.copy(editNation = value) } + fun onEditBundeslandChange(value: String) { uiState = uiState.copy(editBundesland = value) } fun onEditVornameChange(value: String) { uiState = uiState.copy(editVorname = value)