chore: erweitere Pferd-, Funktionär- und Reiter-Modelle um neue Felder, verbessere UI und Suche

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
Stefan Mogeritsch 2026-04-21 13:47:35 +02:00
parent 574f8c470c
commit 2662d4e82e
10 changed files with 399 additions and 196 deletions

View File

@ -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`).

View File

@ -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
)

View File

@ -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) {
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
)
}
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))
}
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
)
}
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))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
MsTextField(
value = state.editRichterNummer,
onValueChange = onRichterNummerChange,
label = "Richter-Nummer",
modifier = Modifier.width(300.dp),
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))

View File

@ -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) }

View File

@ -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 = "",

View File

@ -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) {
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))

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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) {
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,

View File

@ -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)